Skip to content

[DRAFT] FluentUI defaults #3856

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed

Conversation

MarvinKlein1508
Copy link
Contributor

Pull Request

📖 Description

I've previously talked about this within some PR or discussion. I would like to be able to change some default behaviour within the FluentUI library, so I don't need to write the same stuff over and over again.

This PR is just meant as a draft to gather your feedback on this purpose.

The new static class allows the user to override any allowed property in their default value. In this example for the FluentCard the AreaRestriced parameter. In my app I never want any FluentCard to restrict it's area.

Other possible properties which come into my mind:
FluentMessageBar -> FadeIn (which I always want to be false)
FluentStack -> Orientation (which I always want to be Vertical in my case)

Those 3 I overrite in any component within my app. There might be more scenarios for different useful default values as well.

@MarvinKlein1508 MarvinKlein1508 marked this pull request as draft June 2, 2025 11:47
@dvoituron
Copy link
Collaborator

  1. You should use the LibraryConfiguration to define your default configurations.

  2. It's dangerous to use SetParametersAsync to modify a parameter, because Blazor triggers this method many times. This can cause performance issues.
    So you need to find a way to apply a default value once: in the OnInitializeAsync method, for example.

@MarvinKlein1508
Copy link
Contributor Author

@dvoituron thanks for your input! :) The reason I've used the SetParametersAsync method is because I need to know whether the user has manually supplied the parameter. Since you still should be able to overwrite the default if you need the other behaviour in certain scenarios.

You got any idea how to determine if the user has specified the parameter or not?

@dvoituron
Copy link
Collaborator

The reason I've used the SetParametersAsync method is because I need to know whether the user has manually supplied the parameter.

I know why you place this code in this method. But it can't add other problems 😁

It depends on the use cases, but with AreaRestricted the simplest way is probably to turn this property into bool? and in OnInitialize, to determine if this property is null to assign the default value.
Be careful to stay compatible with the existing: by default it should remain AreaRestricted = true.

@MarvinKlein1508
Copy link
Contributor Author

@dvoituron how about this approach? :) It works fine for me.

However I cannot get the Unittests to work since they require now the injection of the LibraryConfiguration

@vnbaaij
Copy link
Collaborator

vnbaaij commented Jun 2, 2025

@MarvinKlein1508 we already have tests where the library configuration is being injected. You should be able to find them with a search on the test project.

@MarvinKlein1508
Copy link
Contributor Author

@vnbaaij alright. Tests have been updated and all have passed. What do you think about this approach in general?

@MarvinKlein1508
Copy link
Contributor Author

Personally I would create sub classes for the components like FluentCardDefaults and give instances to the LibraryConfiguration. This way we can group multiple attributes logically together and we keep the naming simple.

So instead of:

public bool DefaultFluentCardAreaRestricted { get; set; } = true;

I would do:

public FluentCardDefaults FluentCardDefaults { get; set; } = new();

public class FluentCardDefaults
{
    public bool AreaRestricted { get; set; } = true;
}

@vnbaaij
Copy link
Collaborator

vnbaaij commented Jun 2, 2025

I had not actually looked at the code yet, but now that I have, I do not like that we as maintainers are 'determining' what the default are/should be.

What I propose to do instead is to create an attribute like [FluentDefault("{}componentname")] that can be set by the library users themselves to specify a default by using a sort of extension class. The attribute would be applied on a property of the same type as the one you want to set the default for.

Code could look something like:

public static class FluentDefaults {

  [FluentDefault("FluentStack")]
  public Orientation Orientation {get; private set} = Orientation.Vertical
}

We then create code that runs on startup that scans the assemblies for this attribute and applies them though the config to the components. Or to make scanning faster, we could dictate a specific namespace where the class/properties need to be in.
What do you think?

@dvoituron
Copy link
Collaborator

We then create code that runs on startup that scans the assemblies for this attribute and applies them though the config to the components. Or to make scanning faster, we could dictate a specific namespace where the class/properties need to be in.
What do you think?

You then need to use reflection to retrieve the values, which may cause performance issues and also problems with the Blazor WASM compilation process (trimming). I find that using options specified when starting the application (Program.cs) is a good alternative.

In addition, the use of static properties can also cause problems.

@MarvinKlein1508
Copy link
Contributor Author

I had not actually looked at the code yet, but now that I have, I do not like that we as maintainers are 'determining' what the default are/should be.

You already do so. There are many properties which have a default value defined from the library itself.

What I like about the LibraryConfig approach is that it is super easy to change on startup or even via appsettings.json

I'm not a hige fan of attributes and reflection either. Everytime I has to use it it always resulted in some hard to find bugs

@MarvinKlein1508
Copy link
Contributor Author

Also how would you handle same property names from different components in this case.

Sorry for any typos I'm currently in the bus on my way home

@MarvinKlein1508
Copy link
Contributor Author

Another idea would be to create an interface which can be registered on startup for esch component.

I this will give you access to all (public) properties of the component. If an interface for this type of component is being registered it will run during OnInitializedAsync

@MarvinKlein1508
Copy link
Contributor Author

But then the same question applies again. How can we determine if the user has provided the Parameter manually.

@vnbaaij
Copy link
Collaborator

vnbaaij commented Jun 2, 2025

You then need to use reflection to retrieve the values, which may cause performance issues and also problems with the Blazor WASM compilation process (trimming). I find that using options specified when starting the application (Program.cs) is a good alternative.

But us (and only us) specifying what defaults there are (like public bool DefaultFluentCardAreaRestricted { get; set; } = true; in LibraryConfiguration in Marvin's code) is not a good approach. We do not want to have that responsibility/cary the burden of maintaining that list. First, who decides what goes in there and second, who decides on what the default value is going to be?

By using a namespace (that we control), I think we can alleviate the trimming problem. That fact that is costs some performance will be negligible and only occurs on startup. In my opinion that is then the 'price' the dev needs to pay for being able to set defaults. But it will be a really small price. And by limiting the reflection to just one namespace, the performance impact will be even smaller.

In addition, the use of static properties can also cause problems.

It's pseudo code...I just want to make clear what the idea is.

@vnbaaij
Copy link
Collaborator

vnbaaij commented Jun 2, 2025

I had not actually looked at the code yet, but now that I have, I do not like that we as maintainers are 'determining' what the default are/should be.

You already do so. There are many properties which have a default value defined from the library itself.

It is not about setting the default value, it is about letting the system know about the existence of a default value for a specific parameter

What I like about the LibraryConfig approach is that it is super easy to change on startup or even via appsettings.json
But again, we need to specify beforhand for which parameters a default value can be provided

I'm not a hige fan of attributes and reflection either. Everytime I has to use it it always resulted in some hard to find bugs

Sorry, but that is a non-argument :), You use [Parameter] all the time and that works pretty reliably

@dvoituron
Copy link
Collaborator

But us (and only us) specifying what defaults there are (like public bool DefaultFluentCardAreaRestricted { get; set; } = true; in LibraryConfiguration in Marvin's code) is not a good approach. We do not want to have that responsibility/cary the burden of maintaining that list. First, who decides what goes in there and second, who decides on what the default value is going to be?

In my opinion, the question is not “how to do it” but “should we do it.” In my opinion, no. FluentUI offers a style of use with default values. It is not a good idea to suggest changing these default values.

However, CardAreaRestricted is, in my opinion, an exception because it fixes a bug in the web component.
But again, we should not allow all (or most) of the default values to be changed.

@vnbaaij
Copy link
Collaborator

vnbaaij commented Jun 2, 2025

Also how would you handle same property names from different components in this case.

As part of the attribute, you specify the Component name as a parameter of the attribute

[FluentDefault(Component="FluentStack")]

@vnbaaij
Copy link
Collaborator

vnbaaij commented Jun 2, 2025

In my opinion, the question is not “how to do it” but “should we do it.” In my opinion, no. FluentUI offers a style of use with default values. It is not a good idea to suggest changing these default values.

I'm more on Marvin's side on this. Why not offer it? With my approach, it always needs to be a conscious decision of the developer to override/change a default. Any (negative) effect of that will be their concern, not ours

@dvoituron
Copy link
Collaborator

I'm more on Marvin's side on this. Why not offer it? With my approach, it always needs to be a conscious decision of the developer to override/change a default. Any (negative) effect of that will be their concern, not ours

Because you won't be able to decide which parameters to include in this list :-)
All parameters? Do you have thousands of parameters? Many of them are not "nullable", so how to detect a default value?
For example, I would like to set a different default FluentButton.Appearance to Accent. How do I do that?

In my opinion, LibraryConfiguration should be an exception.

@MarvinKlein1508
Copy link
Contributor Author

Let me give you some more insights why I want to have any kind of this.

FluentCard

I always find it strange that the area is restricted to the card's height by default. I cannot think of any useful scenario where I actually wanna have this behaviour at all (I'm just used to it to adapt automatically from Bootstrap). That's why I want to have AreaRestricted set to false by default. Otherwise I need to add this parameter on every card within my app.

FluentInputs

I always set Class="w-100" to make the controls 100% width just like I used to have it in Bootstrap. In addition, whenever I use them within an EditForm I set Immediate="true" to allow to submit the form via enter

MessageBar

I use this component as a replacement for Boostrap alerts. Because I display some additional information or warnings by default, I don't want to have them fading in. That's why I always set FadeIn="false"

Stacks

I always use them to align my content vertically with some gaps between them. Therefore I always set Orientation="Orientation.Vertical" and VerticalGap="5"

That's currently all use-cases I have.

Perhaps as additional info: I wrap my parameters line by line and every line counts within the razor editor. I think I need to restart VS like 5 times a day just because the razor editor crashes and doesn't do anything at all. Especially when you have razor files with more than 1000 lines of code.

@vnbaaij
Copy link
Collaborator

vnbaaij commented Jun 2, 2025

@dvoituron I envision it to be used exactly like Marvin describes: Offer a way to always use a FluentStack with Orientation="Orientation.Vertical". With what I have in mind he can specify that without any interference needed from our side.
So he creates a class with a property that has the FluentDefault attribute (with a parameter that ties it to the specific component name to target) and uses the same signature as the components property with a default value he choses.

All we need to do is make sure that as part of the initialization, the component applies any provided defaults. If that would lead to a malfunctioning component, then it is up to the dev to not use that deautl value or change it or whatever. I believe that doesn't have to be our problem. I created a PR for this code idea with some GH Copilot help: PR #3857

@MarvinKlein1508
Copy link
Contributor Author

@vnbaaij there is still one big issue to overcome. How would you check if I want to use another value just one time. Lets say I define my default value to be Orientation="Orientation.Vertical" but on one page I actually provide the parameter with Orientation="Orientation.Horizontal". How would you determine if the parameter has already been set manually by the user?

@vnbaaij
Copy link
Collaborator

vnbaaij commented Jun 2, 2025

@vnbaaij there is still one big issue to overcome. How would you check if I want to use another value just one time. Lets say I define my default value to be Orientation="Orientation.Vertical" but on one page I actually provide the parameter with Orientation="Orientation.Horizontal". How would you determine if the parameter has already been set manually by the user?

Good question. Let me think about that. In the mean time, please check the PR that was created and lets discuss on that approach there.

From the docs in the other PR:

  1. The service only sets defaults if the current parameter value is "unset" (null for reference types, default value for value types)
  2. Explicitly provided parameter values always take precedence

@dvoituron
Copy link
Collaborator

@MarvinKlein1508 I added this feature in dev-v5. See #3875
Can you use this Preview version or would you like to create a PR for v4 using the same files? In this case, two PRs are also required: one to add the main classes and a second to modify ALL constructors.

I suggest closing this PR for now.

@MarvinKlein1508
Copy link
Contributor Author

@dvoituron I am fine waiting for v5. In my current application I already have provided the parameters everywhere.

Before I need to touch it twice I rather wait for the v5 release. In theory I could already use v5 in my app. I'm only waiting for some components to get startet (navigation, dropdown and autocomplete)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants