WPF - Elegant way of disabling and enabling different controls based on different states of the Model using MVVM
Asked Answered
Z

3

6

I am looking for an elegant solution for the following problem.

Let's assume we have a (View)Model with the following boolean properties:

  • Alpha
  • Beta
  • Gamma
  • Delta

Next I have 5 controls on the surface that shall only be visible when a condition based on those properties are met. Of course, as soon as one of those properties is updated the change should be propagated immediatelly:

  • ControlA -> Alpha && ( Beta || Gamma )
  • ControlB -> Delta
  • ControlC -> Delta || Beta
  • ControlD -> Gamma && Alpha && Delta
  • ControlE -> Alpha || Gamma

The only solution I came up with so far is using MultiValueConverters.

Example for ControlA:

<ControlA>
   <ControlA.Visibility>
      <MultiBinding Converter={StaticResource ControlAVisibilityConverter}>
          <Binding Path="Alpha"/>
          <Binding Path="Beta"/>
          <Binding Path="Gamma"/>
      </MultiBinding>
   </ControlA.Visibility>
</ControlA>

This ControlAVisibilityConverter checks for condition "Alpha && ( Beta || Gamma )" and returns the appropriate value.

It does work.. well.. but maybe you can come up with a more elegant solution?

Thank you, TwinHabit

Zecchino answered 21/9, 2010 at 21:8 Comment(1)
I think it is a good approachEpicurus
E
5

Writing a converter for each rule puts your business logic two places in this case (in the converter and the view model). I suggest creating a property/flag for each control in your ViewModel with INotifyPropertyChanged events to decide whether the control is visible (or other behaviour).

Note, that when you look at my viewmodel (below) you will see that I expose properties of type bool and Visibilty.

If you need to use the property as a general rule use bool and a DataTrigger.

public bool ControlD

If you only need to control visibility you can bind to Visibility directly:

public Visibility ControlA

UPDATE: Because of the comment by @Wallstreet Programmer, I added another option to use a BooleanVisibilityConverter. I updated the fifth control below to reflect how to use a converter. I added the code for the converter at the bottom.

Here is a test Window in XAML:

<Window x:Class="ControlVisibleTrigger.Views.MainView"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Main Window" Height="400" Width="800">
  <Window.Resources>
    <Style x:Key="DropDownStyle" TargetType="TextBox">
        <Setter Property="Visibility" Value="Hidden"/>
        <Style.Triggers>
            <DataTrigger Binding="{Binding ControlC}" Value="True">
                <Setter Property="Visibility" Value="Visible"/>
            </DataTrigger>
        </Style.Triggers>
    </Style>
  </Window.Resources>
  <DockPanel>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <StackPanel Grid.Row="0">
            <CheckBox IsChecked="{Binding Path=Alpha,Mode=TwoWay}" Content="Alpha"/>
            <CheckBox IsChecked="{Binding Path=Beta,Mode=TwoWay}" Content="Beta"/>
            <CheckBox IsChecked="{Binding Path=Gamma,Mode=TwoWay}" Content="Gamma"/>
            <CheckBox IsChecked="{Binding Path=Delta,Mode=TwoWay}" Content="Delta"/>
        </StackPanel>
        <TextBox Grid.Row="1" Visibility="{Binding Path=ControlA}" Text="Binding to visibility"/>
        <Button Grid.Row="2" Visibility="{Binding Path=ControlB}" Content="Binding to visibility"/>
        <TextBox Grid.Row="3" Style="{StaticResource DropDownStyle}" Text="Using WindowResource DataTrigger"/>
        <TextBox Grid.Row="4" Text="Using Local DataTrigger">
            <TextBox.Style>
              <Style TargetType="TextBox">
                <Setter Property="Visibility" Value="Hidden"/>
                <Style.Triggers>
                    <DataTrigger Binding="{Binding ControlD}" Value="True">
                        <Setter Property="Visibility" Value="Visible"/>
                    </DataTrigger>
                </Style.Triggers>
              </Style>
            </TextBox.Style>
        </TextBox>
        <Button Grid.Row="5" 
                Content="Press me" 
                Visibility="{Binding Path=ControlE, Converter={StaticResource booleanVisibilityConverter}, ConverterParameter=True, Mode=OneWay}">
    </Grid>
  </DockPanel>
</Window>

Here is the ViewModel:

public class MainViewModel : ViewModelBase
{
  public MainViewModel()
  {
  }

  private bool _alpha = true;
  public bool Alpha
  {
     get
     {
        return _alpha;
     }
     set
     {
        _alpha = value;
        OnPropertyChanged("ControlA");
        OnPropertyChanged("ControlB");
        OnPropertyChanged("ControlC");
        OnPropertyChanged("ControlD");
        OnPropertyChanged("ControlE");
     }
  }

  private bool _beta = true;
  public bool Beta
  {
     get
     {
        return _beta;
     }
     set
     {
        _beta = value;
        OnPropertyChanged("ControlA");
        OnPropertyChanged("ControlB");
        OnPropertyChanged("ControlC");
        OnPropertyChanged("ControlD");
        OnPropertyChanged("ControlE");
     }
  }

  private bool _gamma = true;
  public bool Gamma
  {
     get
     {
        return _gamma;
     }
     set
     {
        _gamma = value;
        OnPropertyChanged("ControlA");
        OnPropertyChanged("ControlB");
        OnPropertyChanged("ControlC");
        OnPropertyChanged("ControlD");
        OnPropertyChanged("ControlE");
     }
  }

  private bool _delta = true;
  public bool Delta
  {
     get
     {
        return _delta;
     }
     set
     {
        _delta = value;
        OnPropertyChanged("ControlA");
        OnPropertyChanged("ControlB");
        OnPropertyChanged("ControlC");
        OnPropertyChanged("ControlD");
        OnPropertyChanged("ControlE");
     }
  }

  public Visibility ControlA
  {
     get
     {
        Visibility result = Visibility.Hidden;
        if ( Alpha && (Beta || Gamma))
        {
           result = Visibility.Visible;
        }
        return result;
     }
  }

  public Visibility ControlB
  {
     get
     {
        Visibility result = Visibility.Hidden;
        if ( Delta )
        {
           result = Visibility.Visible;
        }
        return result;
     }
  }

  private bool _controlC = false;
  public bool ControlC
  {
     get
     {
        return Delta || Beta;
     }
  }

  private bool _controlD = false;
  public bool ControlD
  {
     get
     {
        return Gamma && Alpha && Delta;
     }
  }

  private bool _controlE = false;
  public bool ControlE
  {
     get
     {
        return Alpha || Gamma;
     }
  }
}

Here is the converter:

public class BooleanVisibilityConverter : IValueConverter
{
  public object Convert( object value, Type targetType, object parameter, System.Globalization.CultureInfo culture )
  {
    if( ( value == null ) || ( !( value is bool ) ) )
      return Binding.DoNothing;

    Visibility elementVisibility;
    bool shouldCollapse = ( ( bool )value );

    if( parameter != null )
    {
      try
      {
        bool inverse = System.Convert.ToBoolean( parameter );

        if( inverse )
          shouldCollapse = !shouldCollapse;
      }
      catch
      {
      }
    }

    elementVisibility = shouldCollapse ? Visibility.Collapsed : Visibility.Visible;
    return elementVisibility;
  }

  public object ConvertBack( object value, Type targetType, object parameter, System.Globalization.CultureInfo culture )
  {
    throw new NotImplementedException();
  }
}
Entrant answered 22/9, 2010 at 14:38 Comment(4)
If we are talking about best practice when it comes to MVVM the VM's shouldn't have view specific logic in them, like Visibility return types. Those properties should be of type bool. In the view, you would then use a BooleanToVisibilityConverter to collapse controls.Selfhelp
I agree, I'd use the bool option as well.Entrant
Thanks @Wallstreet Programmer, I added the converter as you suggested to this answer.Entrant
Thank you very much for your proposal. The only thing I don't like about it somewhat refers to what "Wallstreet Programmer" wrote. Still, the ViewModel has to have knowledge about the controls. It is not enough to simply expose the basic properties (alpha, beta...). Still, the ViewModel dictates which Control is visible at any state of the ViewModel. That's why I chose the way I suggested initially. I agree that the business logic of deciding whether the control is visible or not is seperated in two files but the ViewModel is 100% decoupled from the View... what do you guys think?Zecchino
S
3

Assuming that there's a business logic reason for whether or not the controls should be displayed I'd definitely have the logic stored as a bool in the ViewModel (though I'd name it according to the business logic e.g.: CriteriaA not ControlAVisible). This allows easy unit testing to ensure that the Criteria are set correctly as alpha, beta, gamma and delta change. Other than that I'd agree with Wallstreet Programmers answer (though I don't have the rep to comment or vote his response).

Sweptwing answered 24/9, 2010 at 14:4 Comment(0)
R
0

If the controls support commands (for example if they are buttons), use command pattern. With RelayCommand (look it up), you can specify the condition under which the control is enable with an lambda expression (which is exactly what you need). It needs some code-behind though.

Renegado answered 21/9, 2010 at 21:53 Comment(1)
Actually this approach does not work. First not all of my controls make use of the ICommand pattern. Second of all as far as I know ICommand CanExecute==false only disables controls. But I want to be free to choose whether I want to hide or disable a control. Additionally I need the visibility of my control to be updated immediatelly when the ViewModel changes. That is not given with CanExecute (except if you constantly call CommandManager.Requery...)Zecchino

© 2022 - 2024 — McMap. All rights reserved.