dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
22.14k stars 1.74k forks source link

Button IsEnabled property in Windows Platform #7377

Closed jcmontoya closed 1 year ago

jcmontoya commented 2 years ago

Description

When I set up the Button's "IsEnabled" property, programmatically. The expected behavior is working in all platforms. However, the formatting of the button in the Windows Platform gets lost after a couple of changes. In the Android platform it works as expected.

Steps to Reproduce

  1. Create a new Maui App
  2. Add a Grid and a StackLayout with two buttons
  3. Disable of the buttons by setting IsEnabled="False"
  4. Add code to programmatically change the is enabled property.
    <Grid  BackgroundColor="Black">
          <StackLayout  Orientation="Horizontal" HorizontalOptions="Center">
            <Button x:Name="btn1" Text="Start" WidthRequest="100" Background="Green" Clicked="btn1_Clicked"></Button>
            <Button x:Name="btn2" Text="Stop" WidthRequest="100" Background="Green" Clicked="btn2_Clicked" IsEnabled="False"></Button>
        </StackLayout>
    </Grid>
   private void btn1_Clicked(object sender, EventArgs e)
    {
        btn1.IsEnabled = false;
        btn2.IsEnabled = true;
    }

    private void btn2_Clicked(object sender, EventArgs e)
    {
        btn1.IsEnabled = true;
        btn2.IsEnabled = false;
    }

5.Click on the buttons a couple of times to enable and disable them. After the first cycle both buttons will look disabled. the functionality will still be working but it is confusing for the user. This only happens in windows, not in Android or IOS.

Version with bug

Release Candidate 3 (current)

Last version that worked well

Release Candidate 3 (current)

Affected platforms

Windows

Affected platform versions

Windows 11

Did you find any workaround?

No

Relevant log output

No response

kristinx0211 commented 2 years ago

verified repro on windows. repro project: MauiApp36.zip

wbaldoumas commented 2 years ago

I am also experiencing this same issue on Windows builds. This appears to be happening on Windows 10 in addition to Windows 11 (as reported by @jcmontoya).

MagicAndre1981 commented 2 years ago

this also never really worked well in XF, so there I used VisualStateManager to change the IsEnabled property and here the change always worked. But in MAUI VisualStateManager is broken (#8003), so I can't use it.

Has anyone found a working way to change button states correctly?

ghost commented 2 years ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

LuoyeAn commented 2 years ago

Need fix the issue asap, it also affects Android platform

ascmt commented 2 years ago

When is this issue going to be addressed? This issue is now 5 months, is a major piece of broken functionality on Windows, no known workaround, no official response, and sits unassigned and with no milestone. For all practical purposes this is looks like a WNF, the silence is speaking volumes.

jsuarezruiz commented 2 years ago

issue-7377 Could you try again with the latest version (net7 rc2)?. Cannot reproduce the issue.

ghost commented 2 years ago

Hi @jcmontoya. We have added the "s/needs-info" label to this issue, which indicates that we have an open question for you before we can take further action. This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

ascmt commented 2 years ago

@jsuarezruiz Does your last comment imply that this issue will NOT ever be addressed in net6?

NirabhraDas013 commented 1 year ago

Seems the issue happens if the button is re enabled after an await method.

I'm attaching a screenshot of my code snippet for reference.

The IsEnabled = True works as intended if there was no await call between setting it false and true. But if there is an await call before resetting it to true, the visual element does not reset.

Image Reference image

Catherine25 commented 1 year ago

I also have the same issue with .NET 6, attaching a screenshot. _grid.Children is just a typed version of the Grid.Children, I use _grid.Children.Select(x => (Button)x).

image

IsVisible works though.

shizukudrops commented 1 year ago

I encountered the same issue with .NET 7. A repo for reproduction: ButtonIssueSampleNet7.zip

Capture: https://user-images.githubusercontent.com/25539067/200715698-6048860a-cd40-4849-a77c-2dab88de872d.mp4

5CoJL commented 1 year ago

I tried fiddling around to find a (somewhat stupid) workaround, as in actually having two buttons, one permanently enabled and one permanently not, and binding both their IsVisible properties to the same boolean with an inverse converter.

GitHub

The correct buttons are displayed, yet the button with the enabled status fixed at "True" STILL grays itself out when it appears. It seems something is forcing the IsEnabled property to false, perhaps with these await statements as mentioned by @NirabhraDas013.

Not quite to be honest. The button itself is not disabled. The click function works as intended but the visuals are stuck at disabled status.

Edit : I meant "it seems something is forcing the BackgroundColor property to match an "IsEnabled = false" state. The button IsEnabled status in itself is working correctly."

The (very ugly) workaround that does work however is to make the button color transparent and place a boxview right behind your button, with a binded Color property. If it's stupid but it works...

stupidworkaround

So writing a custom control "CustomButton" composed of a boxview and a button could be a somewhat decent workaround ?

borrrden commented 1 year ago

I can confirm the same issue and the analysis of await vs not seems to be spot on.

NirabhraDas013 commented 1 year ago

@5CoJL --

It seems something is forcing the IsEnabled property to false

Not quite to be honest. The button itself is not disabled. The click function works as intended but the visuals are stuck at disabled status.

The (very ugly) workaround that does work however is to make the button color transparent and place a BoxView right behind your button, with a binded Color property. If it's stupid but it works...

I ended up doing something similar. but maybe a bit more complicated than it needed to be (being new to . NET doesn't help haha). I positioned a Label with a border to set it look like the button and turned it on and fixed Text and grayed background. This wat the Button is never disabled just hidden from view

@borrrden -- I'm not a .NET developer by trade. I'm usually Unity Developer. I've tried my hands at Avalonia and now trying MAUI as we need a cross platform desktop app. I might have missed something in the await analysis. But I have tried multiple times with new projects and the behavior before and after the await call remains same for me. Could you share your findings please so I can try to look from a different angle

NirabhraDas013 commented 1 year ago

@Catherine25 -- Have you checked if the button click works after the IsEnabled = true call? For me only the visual was stuck at the disabled state but the button click still worked so the button itself had been enabled

Catherine25 commented 1 year ago

@NirabhraDas013,

I just double checked. For me the click works only when another button was pressed after mine. Clicking anything else and minimizing the window will not make the same effect.

So, 2+ times clicking on A button will still raise Click event only 1 time. When I click for example A -> B -> A, Click event might be raised 2+ times.

borrrden commented 1 year ago

@NirabhraDas013 I take it back, It's not completely that cut and dry. Here is the situation in my application:

I have two buttons (say, A and B) and an Entry field. At the start A is enabled, and B is disabled. They are bound to the same backing variable, just one is run through an InvertedBoolConverter.

  1. Pushing A sets the backing variable to true, which sets A to disabled and B to enabled, and then performs an await. The visual states are as expected (A gray B blue)
  2. Changing the text of the entry field resets the backing variable to false, which sets A to enabled and B to disabled. This results in both buttons being in the disabled visual state (gray) which is wrong.
  3. Repeating 1 puts the buttons back into the correct state (A gray B blue)
MagicAndre1981 commented 1 year ago

I use VisualStateManager to change the change the state via VisualStateManager.GoToState(ButtonLog1l, "Disabled");. My mistake was to not include <VisualStateGroupList> entry now everything works.

<Button x:Name="Button1" Text = "Foo" Clicked="OnButton1Clicked">
<VisualStateManager.VisualStateGroups>
    <VisualStateGroupList>
        <VisualStateGroup Name="ValidityStates">
            <VisualState Name="Enabled" />
            <VisualState Name="Disabled">
                <VisualState.Setters>
                    <Setter Property="IsEnabled" Value="False" />
                </VisualState.Setters>
            </VisualState>
        </VisualStateGroup>
    </VisualStateGroupList>
</VisualStateManager.VisualStateGroups>
</Button>
tomas-andersson commented 1 year ago

IsEnabled works for me if I never ever modify it synchronously from a Clicked event.

Just awaiting anything does not guarantee that the rest of the method runs asynchronously. I must await Task.Yield to be sure.

This is only necessary on Windows. When running on Android, IsEnabled works as expected.

private async void OnMyButtonClicked(object sender, EventArgs e) { await Task.Yield(); // without this line all future calls (in all methods) to buttonDisconnect.IsEnabled will not work buttonDisconnect.IsEnabled = false; ` // the rest of your code here }`

shizukudrops commented 1 year ago

@tomas-andersson await Task.Yield() works for me! Thank you very much!

leojablon1 commented 1 year ago

Did anyone find a workaround for commands?

    [RelayCommand(CanExecute = nameof(CanExecute))]
    private async Task Second()
    {
        await Task.Yield();
        CanExecute = false;
        await Task.Delay(3000);
        CanExecute = true;
    }

Is still not working for me. The binded button is stuck in the greyed-out state, even though it becomes clickable after the 3s delay.

Catherine25 commented 1 year ago

Today updated my project to .net 7 (minimum target windows framework 10.0.22000.0) Issue is still there.

MagicAndre1981 commented 1 year ago

Today updated my project to .net 7 (minimum target windows framework 10.0.22000.0) Issue is still there.

use VisualStateManager, this works fine for me and also worked in XF, but in MAUI compared toXF I need to add <VisualStateGroupList>

chaddoncooper commented 1 year ago

I really don't want to be writing all the boilerplate that comes with visual states just to convey an enabled/disabled button.

vaporz commented 1 year ago

I found a workaround: using the command interface. https://learn.microsoft.com/en-us/dotnet/maui/user-interface/controls/button?view=net-maui-7.0#use-the-command-interface Change button's state via function "canExecute", works for me.

MagicAndre1981 commented 1 year ago

I really don't want to be writing all the boilerplate that comes with visual states just to convey an enabled/disabled button.

in XF I also sometime got issues enabling/disabling buttons and here VisualStateManager always worked. So you should use the proper solution VisualStateManager

danielancines commented 1 year ago

Morning guys, any update on this? Still happens on VS 2022 17.4.2 and .NET 7.0.100

dan-matthews commented 1 year ago

If it helps, I noticed a weird addition to this issue. Firstly, yes the issue seems to only repro if an async method that actually does something is called (for example, await DisplayAlert(...)). So, for example, we start with IsEnabled on the button true:

image

if IsEnabled is set false then the button shows disabled. Then the DisplayAlert is called. You will see that the button is, as expected, disabled.

image

We click 'OK' on the alert, then IsEnabled is set true. Now it stays in the visual state for disabled, even though it is actually enabled as it is clickable again:

image

The weird addition is that if I simply copy-paste to put TWO buttons on the page, then the one I don't click works as expected. However, the one I clicked stays in the disabled state. See example below. Firstly, before we click the first button:

image

Next we click the first button. Note that BOTH buttons disable, as they should (I am binding to an enabled property):

image

We click 'OK' and the alert closes... but the button I clicked stays disabled. The one I did NOT click shows the enabled visual state though!

image

It appears that it's something to do with the actual click handling on windows somehow overriding the visual state callback once it's done, or something related to that.

hartez commented 1 year ago

The initially reported issue and the version of the issue with async/await are fixed by #11840. The fix should be in the next service release.