Skip to content

Dialog: Use ParameterState, make DialogOptions immutable#10066

Merged
ScarletKuro merged 18 commits intoMudBlazor:devfrom
ScarletKuro:dialog_improve
Oct 22, 2024
Merged

Dialog: Use ParameterState, make DialogOptions immutable#10066
ScarletKuro merged 18 commits intoMudBlazor:devfrom
ScarletKuro:dialog_improve

Conversation

@ScarletKuro
Copy link
Member

@ScarletKuro ScarletKuro commented Oct 21, 2024

Description

Fixes: #10036
Removes DialogHelperComponent, as it is no longer needed and can interfere with the ParameterState. I don’t believe that Blazored.Modal is using it either: https://github.com/Blazored/Modal/blob/main/src/Blazored.Modal/Services/ModalService.cs
Makes the DialogOptions immutable to prevent consumers from modifying the Options parameter directly. They should use the new approach with the with syntax, as shown in DialogSetOptionsExample_Dialog.razor / DialogToggleFullscreen.razor the examples.

Despite my comment here: #10036 (comment), I still had to add OptionsChanged for two-way binding. While ParameterState was working, one case wasn’t covered, which was this:

private Task ToggleFullscreenAsync()
{
    var options = MudDialog.Options with
    {
        FullScreen = !(MudDialog.Options.FullScreen ?? false)
    };

    return MudDialog.SetOptionsAsync(options);
}

The problem is that if you click on ToggleFullscreenAsync a second time, it wouldn’t work as expected. At that point, only ParameterState would update, while MudDialog.Options.FullScreen would not, since we never write to it. This means you wouldn’t be able to toggle FullScreen when relying on MudDialog.Options.FullScreen, as its value never updates.

This could be solved by:

  1. Introducing GetOptions() in the MudDialogInstance to return _dialogOptionsState.Value.
  2. Breaking the rules and writing to both _dialogOptionsState and Options.

I decided that the most appropriate solution is to introduce two-way binding for the parameters we modify.

To achieve this, I had to create an additional layer, MudDialogInstanceParent, because for two-way binding to work, I need a reference to ComponentBase. I couldn’t do that in the following code:

var dialogInstance = new RenderFragment(builder =>
{
builder.OpenComponent<MudDialogInstance>(0);
builder.SetKey(dialogReference.Id);
builder.AddAttribute(1, nameof(MudDialogInstance.Options), options);
builder.AddAttribute(2, nameof(MudDialogInstance.Title), title);
builder.AddAttribute(3, nameof(MudDialogInstance.Content), dialogContent);
builder.AddAttribute(4, nameof(MudDialogInstance.Id), dialogReference.Id);
builder.CloseComponent();
});

This is because it doesn’t provide any API, and OpenComponent doesn’t return the instance. There is also nothing that points to the ComponentBase of what we are currently creating.

How Has This Been Tested?

bunit test

Type of Changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation (fix or improvement to the website or code docs)

Checklist

  • The PR is submitted to the correct branch (dev).
  • My code follows the code style of this project.
  • I've added relevant tests.

@github-actions github-actions bot added bug Unexpected behavior or functionality not working as intended enhancement Adds a new feature or enhances existing functionality (not fixing a defect) in the main library PR: needs review labels Oct 21, 2024
@ScarletKuro ScarletKuro added breaking change This change will require consumer code updates v8 labels Oct 21, 2024
@ScarletKuro
Copy link
Member Author

ScarletKuro commented Oct 21, 2024

Interesting that the sync version Show fails, while async doesn't. Forgot to update it's code...
But before I invest more time in this and add tests for #10036 (works visually)
I need to know if we event want such fundamental changes.
@henon @danielchalmers

Please read carefully the description.

@henon
Copy link
Contributor

henon commented Oct 21, 2024

  1. Introducing GetOptions() in the MudDialogInstance to return _dialogOptionsState.Value.

Isn't this the easiest solution?

I decided that the most appropriate solution is to introduce two-way binding for the parameters we modify.
To achieve this, I had to create an additional layer, MudDialogInstanceParent, because for two-way binding to work, I need a reference to ComponentBase.

Are you sure this is worth it? I mean if a simple GetOptions() can do the job why complicate things with two-way bindings?

@codecov
Copy link

codecov bot commented Oct 21, 2024

Codecov Report

Attention: Patch coverage is 77.21519% with 18 lines in your changes missing coverage. Please review.

Project coverage is 91.15%. Comparing base (336434d) to head (6266371).
Report is 6 commits behind head on dev.

Files with missing lines Patch % Lines
...azor/Components/Dialog/MudDialogContainer.razor.cs 68.96% 10 Missing and 8 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev   #10066      +/-   ##
==========================================
+ Coverage   91.13%   91.15%   +0.02%     
==========================================
  Files         410      411       +1     
  Lines       12459    12466       +7     
  Branches     2419     2419              
==========================================
+ Hits        11354    11364      +10     
+ Misses        560      557       -3     
  Partials      545      545              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@ScarletKuro
Copy link
Member Author

Are you sure this is worth it? I mean if a simple GetOptions() can do the job why complicate things with two-way bindings?

Obviously, GetOptions would be the easiest solution. However, looking from a user perspective, it’s not logical to have two options: Options and GetOptions. People typically do not read breaking change documentation unless something fails to compile, and they often do not look examples either. We get a lot of issues because of that.
If we simply add GetOptions, existing code that uses Options will break, and users might not even notice the problems. I would prefer a more complicated solution on our side if it offers an intuitive API for consumers.
Sadly, you cannot hide a component's parameters, I would agree on this solution if it was possible.
However, I can suggest an alternative approach. Exposing MudDialogInstance was a poor decision from the start, as it is a Blazor component and allowing access to the parent's parameters is a code smell.

We can create a IMudDialogInstance, which exposes only the necessary stuff, and MudDialogInstance can implement this interface. Its explicit implementation will return _dialogOptionsState.Value for Options. We also change CascadingValue to <CascadingValue Value="((IMudDialogInstance)this)" IsFixed="true">.

We will also need to rename the current MudDialogInstance (I’m unsure what to name it) so that users gets a compilation error to force them to change to the new usage from MudDialogInstance to IMudDialogInstance. This way everything will be more beautiful ;)

@henon
Copy link
Contributor

henon commented Oct 21, 2024

We will also need to rename the current MudDialogInstance (I’m unsure what to name it)

Maybe MudDialogInstanceInternal or just DialogInstance ?

@ScarletKuro
Copy link
Member Author

Maybe MudDialogInstanceInternal or just DialogInstance ?

Ig, we can discuss it once the implementation is ready.

@ScarletKuro
Copy link
Member Author

@henon, done. Called it MudDialogContainer.

@ScarletKuro
Copy link
Member Author

Haha, after this change, I want to rewrite the dialogs, especially finally add a IDialogReference<T> that points to a non-generic IDialogReference. Then, you'd return IDialogReference<T> for all ShowAsync<T> APIs and IDialogReference for ShowAsync(Type component). This way, there would be no more Dialog as an object and no more casts. However, it would break the promise of no huge rewrites in v8. Since dialogs are a core part of the system, it would also require a clean up the old API, so I guess I’ll save it for next time.

@ScarletKuro
Copy link
Member Author

For now, I'm casting IMudDialogInstance to MudDialogContainer to access HandleKeyDownAsync

var dialogInstance1 = dialog1.MudDialog.GetDialogContainer();

However, when I will rewrite this for v9, I will use KeyInterceptorService directly. In my opinion, bUnit tests should be closer to real conditions. We rely too much on invoking internal methods directly instead of simulating actions via bUnit.

@henon
Copy link
Contributor

henon commented Oct 22, 2024

@henon, done. Called it MudDialogContainer.

That is a great name

Comment on lines +206 to +212
if (GetDialogOptionsOrDefault.CloseOnEscapeKey.HasValue)
return GetDialogOptionsOrDefault.CloseOnEscapeKey.Value;

if (GlobalDialogOptions.CloseOnEscapeKey.HasValue)
return GlobalDialogOptions.CloseOnEscapeKey.Value;

return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe in the v9 rewrite we could just move the global dialog option values into MudGlobal for consistency.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was my thought as well.

@henon henon added hacktoberfest Hacktoberfest 2021 hacktoberfest-accepted Issues and PRs which were accepted as Hacktoberfest submissions hacktoberfest2024 labels Oct 22, 2024
@ScarletKuro
Copy link
Member Author

ScarletKuro commented Oct 22, 2024

Ideas for v9:

  1. Implement more strongly typed interfaces, such as IDialogReference<T> and potentially IDialogResult<T>.
  2. Introduce MudGlobal for global settings, instead of using cascading parameters.
  3. Remove the old API.
  4. IDialogReference should not expose InjectDialog or InjectRenderFragment, as these are only used internally within the service. Therefore, the public IDialogReference should not expose them, as they can be dangerous, however, the implementation of this interface can expose them.
  5. The DialogService should maintain a list of current dialogs. Currently, this is handled in the dialog provider via DialogInstanceAddedAsync and OnDialogCloseRequested, but we should align the API more closely with IPopoverService, which manages this logic. Providers should only be responsible for displaying the dialogs.
  6. Address the issue of TitleContent not updating automatically, which requires calling MudDialog.StateHasChanged. If this cannot be resolved, consider adding a base class, BaseDialogSomething, which would be an optional base class that provides a more user-friendly API and includes IMudDialogInstance, Cancel and MudDialog.StateHasChanged as just StateHasChanged, etc., automatically.
  7. Make sure API for getting the dialog return value is intuitive. Currently you have to do something like instance.Result.Result which is horrible.

@ScarletKuro
Copy link
Member Author

LGTM

I will add test for #10036 and then merge it.

@henon
Copy link
Contributor

henon commented Oct 22, 2024

Great, I added a point to the ideas:

7. Make sure API for getting the dialog return value is intuitive. Currently you have to do something like instance.Result.Result which is horrible.

@ScarletKuro
Copy link
Member Author

Added to the v8 migration guide at #9953

@ScarletKuro ScarletKuro merged commit c396973 into MudBlazor:dev Oct 22, 2024
@ScarletKuro ScarletKuro deleted the dialog_improve branch October 22, 2024 20:42
@ScarletKuro ScarletKuro linked an issue Oct 23, 2024 that may be closed by this pull request
4 tasks
@hexpoint
Copy link

We ran into a minor issue with unit testing some of our dialogs using bunit. We rendered the dialog instance cascading value in the unit tests previously using MudDialogInstance and the change to IMudDialogInstance prevents rendering. The simple fix was to render it in the unit test using MudDialogContainer which I found in this thread. Something that others might also run into.

LLauter pushed a commit to cannellamedia/MudBlazor that referenced this pull request Dec 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change This change will require consumer code updates bug Unexpected behavior or functionality not working as intended enhancement Adds a new feature or enhances existing functionality (not fixing a defect) in the main library hacktoberfest Hacktoberfest 2021 hacktoberfest2024 hacktoberfest-accepted Issues and PRs which were accepted as Hacktoberfest submissions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MudDialog options state corrupted when one dialog instances another

4 participants