WPF — элегантный способ отключения и включения различных элементов управления на основе разных состояний модели с использованием MVVM.

Я ищу элегантное решение для следующей проблемы.

Предположим, у нас есть (View)Model со следующими булевыми свойствами:

  • Альфа
  • Бета
  • Гамма
  • Дельта

Далее у меня есть 5 элементов управления на поверхности, которые будут видны только при выполнении условия, основанного на этих свойствах. Конечно, как только одно из этих свойств будет обновлено, изменение должно быть немедленно распространено:

  • ControlA -> Альфа && ( Бета || Гамма )
  • ControlB -> Дельта
  • ControlC -> Дельта || Бета
  • ControlD -> Гамма && Альфа && Дельта
  • ControlE -> Альфа || Гамма

Единственное решение, которое я придумал до сих пор, — это использование MultiValueConverters.

Пример для ControlA:

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

Этот ControlAVisibilityConverter проверяет условие «Альфа && (Бета || Гамма)» и возвращает соответствующее значение.

Это работает... хорошо... но, может быть, вы придумаете более элегантное решение?

Спасибо, ТвинХабит


person TwinHabit    schedule 21.09.2010    source источник
comment
Я думаю, это хороший подход   -  person Damian Schenkelman    schedule 22.09.2010


Ответы (3)


Написание преобразователя для каждого правила в этом случае помещает вашу бизнес-логику в два места (в преобразователь и в модель представления). Я предлагаю создать свойство/флаг для каждого элемента управления в вашей ViewModel с событиями INotifyPropertyChanged, чтобы решить, виден ли элемент управления (или другое поведение).

Обратите внимание: когда вы посмотрите на мою модель представления (ниже), вы увидите, что я выставляю свойства типа bool и Visibilty.

Если вам нужно использовать свойство как общее правило, используйте bool и DataTrigger.

public bool ControlD

Если вам нужно только контролировать видимость, вы можете напрямую привязаться к видимости:

public Visibility ControlA

ОБНОВЛЕНИЕ: из-за комментария программиста @Wallstreet я добавил еще один вариант использования BooleanVisibilityConverter. Я обновил пятый элемент управления ниже, чтобы показать, как использовать конвертер. Я добавил код конвертера внизу.

Вот тестовое окно в 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>

Вот 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;
     }
  }
}

Вот преобразователь:

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();
  }
}
person Zamboni    schedule 22.09.2010
comment
Если мы говорим о передовой практике, когда речь идет о MVVM, виртуальные машины не должны иметь в себе специфическую логику представления, например, возвращаемые типы видимости. Эти свойства должны быть типа bool. Затем в представлении вы должны использовать BooleanToVisibilityConverter, чтобы свернуть элементы управления. - person Wallstreet Programmer; 22.09.2010
comment
Я согласен, я бы тоже использовал опцию bool. - person Zamboni; 22.09.2010
comment
Спасибо @Wallstreet Programmer, я добавил конвертер, как вы предложили в этот ответ. - person Zamboni; 23.09.2010
comment
Большое спасибо за ваше предложение. Единственное, что мне в нем не нравится, относится к тому, что написал Wallstreet Programmer. Тем не менее, ViewModel должен знать об элементах управления. Недостаточно просто указать основные свойства (альфа, бета...). Тем не менее ViewModel определяет, какой элемент управления виден в любом состоянии ViewModel. Поэтому я выбрал способ, который предложил изначально. Я согласен с тем, что бизнес-логика принятия решения о том, виден элемент управления или нет, разделена на два файла, но ViewModel на 100% отделен от представления... что вы, ребята, думаете? - person TwinHabit; 23.09.2010

Предполагая, что есть причина бизнес-логики для отображения элементов управления, я бы определенно сохранил логику как логическое значение в ViewModel (хотя я бы назвал ее в соответствии с бизнес-логикой, например: CriteriaA не ControlAVisible). Это позволяет легко проводить модульное тестирование, чтобы убедиться, что Критерии установлены правильно в виде альфа-, бета-, гамма- и дельта-изменений. В остальном я согласен с ответом программистов с Уоллстрита (хотя у меня нет представителя, чтобы прокомментировать или проголосовать за его ответ).

person Jackson Pope    schedule 24.09.2010

Если элементы управления поддерживают команды (например, если они являются кнопками), используйте шаблон команды. С помощью RelayCommand (ищите его) вы можете указать условие, при котором элемент управления включается, с помощью лямбда-выражения (это именно то, что вам нужно). Однако для этого нужен код.

person Matěj Zábský    schedule 21.09.2010
comment
На самом деле этот подход не работает. Во-первых, не все мои элементы управления используют шаблон ICommand. Во-вторых, насколько я знаю, ICommand CanExecute==false отключает только элементы управления. Но я хочу свободно выбирать, хочу ли я скрыть или отключить элемент управления. Кроме того, мне нужно, чтобы видимость моего элемента управления обновлялась немедленно при изменении ViewModel. Это не дается CanExecute (за исключением случаев, когда вы постоянно вызываете CommandManager.Requery...) - person TwinHabit; 22.09.2010