Ben
Ben

Reputation: 41

WPF Expander ignoring data binding after user interaction

I have a WPF application with an expander where I want the IsExpanded property to change based on an enum value in my view model i.e. I want the expander to open or close based on this enum property. I am using data triggers in the xaml to update the IsExpanded property based on the value of my enum backing property.

<Expander Header="Information and Procedures"
                              BorderThickness="0"
                              Margin="5">
                        <Expander.Style>
                            <Style TargetType="{x:Type Expander}" BasedOn="{StaticResource MetroExpander}">
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding DataContext.Item.Match, RelativeSource={RelativeSource AncestorType={x:Type UserControl}, Mode=FindAncestor}, Mode=TwoWay}" 
                                                 Value="{x:Static enum:Match.Multiple}">
                                        <Setter Property="IsExpanded" Value="False"/>
                                    </DataTrigger>
                                    <DataTrigger Binding="{Binding DataContext.Item.Match, RelativeSource={RelativeSource AncestorType={x:Type UserControl}, Mode=FindAncestor}, Mode=TwoWay}" 
                                                 Value="{x:Static enum:Match.None}">
                                        <Setter Property="IsExpanded" Value="True"/>
                                    </DataTrigger>
                                    <DataTrigger Binding="{Binding DataContext.Item.Match, RelativeSource={RelativeSource AncestorType={x:Type UserControl}, Mode=FindAncestor}, Mode=TwoWay}" 
                                                 Value="{x:Static enum:Match.Exact}">
                                        <Setter Property="IsExpanded" Value="True"/>
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </Expander.Style>
                        <WrapPanel Orientation="Horizontal" Background="Transparent">
                            <view:InformationView Margin="0 0 20 0"/>
                            <view:ProceduresView Margin="0 0 0 0" />
                        </WrapPanel>
                     </Expander>

This seems to work until we manually expand the expander. After this it seems as if the data triggers are now ignored and the manually set IsExpanded property will not change until the user manually opens or closes the expander again.

I want the user to be able to manually expand or close it, but I want the data triggers to take priority over the user input.

Note: I don't want the value of Match to be changed by the IsExpanded property. Match is updated separately, but its value affects IsExpanded

Upvotes: 3

Views: 713

Answers (2)

user2261015
user2261015

Reputation: 458

The view model should not depend on the view. That is, you should not add an IsExpanded property as suggested by Scroog's answer.

I resolved a very similar problem by a Behaviour class that changes IsExpanded depending on the IsEnabled property of the Expander. So this implemented in view only. The following class is just an example and may be enhanced by adding a dependency property that binds to any property of the view model that will be used instead of checking IsEnabled.

public class AutoExpandBehaviour 
{
    public static readonly DependencyProperty AutoExpandProperty =
            DependencyProperty.RegisterAttached(
            "AutoExpand",
            typeof(bool),
            typeof(AutoExpandBehaviour),
            new UIPropertyMetadata(false, AutoExpandChanged));

    public static bool GetAutoExpand(Expander expander)
    {
        return (bool)expander.GetValue(AutoExpandProperty);
    }

    public static void SetAutoExpand(Expander expander, bool value)
    {
        expander.SetValue(AutoExpandProperty, value);
    }

    static void AutoExpandChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
    {
        var expander = depObj as Expander;
        if (expander == null)
        {
            return;
        }

        if (e.NewValue is bool == false)
            return;

        if ((bool)e.NewValue)
        {
            expander.IsEnabledChanged += Item_IsEnabledChanged;
        }
        else
        {
            expander.IsEnabledChanged -= Item_IsEnabledChanged;
        }
    }

    private static void Item_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        var expander = sender as Expander;
        if (expander != null)
        {
            expander.IsExpanded = (bool)e.NewValue;
        }
    }
}

And using in XAML as

        <Expander IsEnabled="{Binding IsFunctionAvailable}" 
              local:AutoExpandBehaviour.AutoExpand="True"
              ... >
            ...
        </Expander>

Upvotes: 0

Scroog1
Scroog1

Reputation: 3579

This is because the binding is on the DataTrigger, not the IsExpanded property, so changing IsExpanded has no effect no the DataTrigger bound property.

Bind to the IsExpanded property and use a custom IValueConverter to translate in both directions between the source enum and the expander bool.

See: https://learn.microsoft.com/en-us/uwp/api/windows.ui.xaml.data.ivalueconverter

As a nice side effect, you'll also end up with shorter, easier to read XAML.

UPDATE

To achieve the desired behaviour, have a boolean property on the viewmodel for the expanded state, that is bound 2-way to the IsExpanded property of the Expander and change it in the viewmodel when the Match property changes. That way both the user and the viewmodel can modify the state of the Expander.

Something like:

public bool IsExpanded
{
    get => _isExpanded;
    set
    {
        _isExpanded = value;
        OnPropertyChanged(nameof(IsExpanded));
    }
}

public Match Match
{
    get => _match;
    set
    {
       _match = value;
       switch (value)
       {
           case Exact:
           case None:
               IsExpanded = true;
               break;
           case Multiple:
               IsExpanded = false;
               break;
       }
    }
}

Upvotes: 2

Related Questions