wpfxamltriggersmultidatatrigger

Why does Button.IsEnabled not start as false or reset to false when the MultiDataTrigger conditions are not satisfied?


I have a usercontrol hosted in an office Taskpane, for a Word addin.

I have tried following the answer to DataTrigger to make WPF Button inactive until TextBox has value and the answer to Cleanest way to bind a Button's visibility to the contents of two textboxes, in order to enable my button when exactly two textboxes have non-null contents.

The converter:

using System.Windows.Data;
using System.Globalization;

namespace RetrofitDocumentTool.Converter
{
    [ValueConversion(typeof(String), typeof(Boolean))]
    class StringToBooleanConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            string val = (string)value;
            bool result = !string.IsNullOrEmpty(val);
            return result;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException("This converter is oneway only.");
        }
    }
}

The XAML:

<UserControl
    ...>

    <UserControl.Resources>
        <Style x:Key="ErrorLabel" TargetType="Label">
            ...
        </Style>

        <Style x:Key="StandardLabel" TargetType="Label">
            ...
        </Style>

        <Style TargetType="TextBox">
            ...
        </Style>

        <conv:StringToBooleanConverter x:Key="StringToBoolean"/>
    </UserControl.Resources>

    <DockPanel LastChildFill="True">
        <Border CornerRadius="8">
            <Grid
                ...
                ...
                ...

                <TextBox
                    Name="Serial_Number"
                    ...>
                </TextBox>
                <TextBox
                    Name="Job_Number"
                    ...>
                </TextBox>

                <Button
                    ...>

                    <Button.Style>
                        <Style TargetType="{x:Type Button}">
                            <Style.Triggers>
                                <MultiDataTrigger>
                                    <MultiDataTrigger.Conditions>
                                        <Condition Binding="{Binding Text, ElementName=Serial_Number, Converter={StaticResource StringToBoolean}}" Value="True"/>
                                        <Condition Binding="{Binding Text, ElementName=Job_Number, Converter={StaticResource StringToBoolean}}" Value="True"/>
                                    </MultiDataTrigger.Conditions>
                                    <Setter Property="IsEnabled" Value="True"/>
                                </MultiDataTrigger>
                            </Style.Triggers>
                        </Style>
                    </Button.Style>

                </Button>
            </Grid>
        </Border>
    </DockPanel>

</UserControl>

The Problem:

The converters return true when there is something in the textbox, and if both textboxes have something in them both converters fire and the setter should be applied. I know this happens because I can set the IsEnabled property to False in the setter and it will deactivate the button, where the initial state was activated.

The reverse does not happen: setting the IsEnabled property to True does not enable the button, where the initial state was deactivated (which I did by setting the IsEnabled property to False directly on the button). I figured this would act as the default state, and the Setter would override it. This does not seem to be the case.

Some oddities I've noticed:

The second condition does not seem to fire until the first one does. Ie, if I enter some data into the second textbox before touching the first textbox, the converter does not fire at all. If I think enter data into the first textbox, both converters fire.

Is there a way to do enable the button only when data exists in the textboxes? Do I actually need to write out every combination of cases: (True/True = Enabled, True/False = Disabled, False/True = Disabled, False/False = Disabled) ? Is this a case of a typo or some silly logic error on my part? I really can't look at this with unbiased eyes at the moment.


Solution

  • Try updating your Style to

    <Style TargetType="{x:Type Button}">
      <!-- New Bit -->
      <Setter Property="IsEnabled" Value="False"/>
      <!-- End of New Bit -->
        <Style.Triggers>
            <MultiDataTrigger>
                <MultiDataTrigger.Conditions>
                    <Condition Binding="{Binding Text, ElementName=Serial_Number, Converter={StaticResource StringToBoolean}}" Value="True"/>
                    <Condition Binding="{Binding Text, ElementName=Job_Number, Converter={StaticResource StringToBoolean}}" Value="True"/>
                </MultiDataTrigger.Conditions>
                <Setter Property="IsEnabled" Value="True"/>
            </MultiDataTrigger>
        </Style.Triggers>
    </Style>
    

    which I did by setting the IsEnabled property to False directly on the button

    If you set a property on the element, then WPF precedence will make any Trigger's or default setters for that property pretty much inactive.

    Finally it's better to always assign a default value for a property if you're using a Trigger to modify it and make sure you don't have that property mentioned on the element itself.

    In your specific case, you could even do this without using a converter.

    <StackPanel xmlns:sys="clr-namespace:System;assembly=mscorlib">
      <TextBox x:Name="tbOne" />
      <TextBox x:Name="tbTwo" />
      <Button Content="Some Button">
        <Button.Style>
          <Style TargetType="{x:Type Button}">
            <Setter Property="IsEnabled"
                    Value="True" />
            <Style.Triggers>
              <DataTrigger Binding="{Binding ElementName=tbOne,
                                              Path=Text}"
                            Value="{x:Static sys:String.Empty}">
                <Setter Property="IsEnabled"
                        Value="False" />
              </DataTrigger>
              <DataTrigger Binding="{Binding ElementName=tbTwo,
                                              Path=Text}"
                            Value="{x:Static sys:String.Empty}">
                <Setter Property="IsEnabled"
                        Value="False" />
              </DataTrigger>
            </Style.Triggers>
          </Style>
        </Button.Style>
      </Button>
    </StackPanel>