Como vincular propriedades booleanas inversas no WPF?

378

O que tenho é um objeto que possui uma IsReadOnlypropriedade. Se essa propriedade for verdadeira, eu gostaria de definir a IsEnabledpropriedade em um Button (por exemplo) como false.

Eu gostaria de acreditar que posso fazê-lo tão facilmente quanto IsEnabled="{Binding Path=!IsReadOnly}"isso, mas isso não ocorre com o WPF.

Estou relegado a ter que passar por todas as configurações de estilo? Apenas parece muito prolixo para algo tão simples quanto definir um bool para o inverso de outro bool.

<Button.Style>
    <Style TargetType="{x:Type Button}">
        <Style.Triggers>
            <DataTrigger Binding="{Binding Path=IsReadOnly}" Value="True">
                <Setter Property="IsEnabled" Value="False" />
            </DataTrigger>
            <DataTrigger Binding="{Binding Path=IsReadOnly}" Value="False">
                <Setter Property="IsEnabled" Value="True" />
            </DataTrigger>
        </Style.Triggers>
    </Style>
</Button.Style>
Russ
fonte
eh ms fazer coisa boa, mas incompleta
user1005462

Respostas:

488

Você pode usar um ValueConverter que inverte uma propriedade bool para você.

XAML:

IsEnabled="{Binding Path=IsReadOnly, Converter={StaticResource InverseBooleanConverter}}"

Conversor:

[ValueConversion(typeof(bool), typeof(bool))]
    public class InverseBooleanConverter: IValueConverter
    {
        #region IValueConverter Members

        public object Convert(object value, Type targetType, object parameter,
            System.Globalization.CultureInfo culture)
        {
            if (targetType != typeof(bool))
                throw new InvalidOperationException("The target must be a boolean");

            return !(bool)value;
        }

        public object ConvertBack(object value, Type targetType, object parameter,
            System.Globalization.CultureInfo culture)
        {
            throw new NotSupportedException();
        }

        #endregion
    }
Chris Nicol
fonte
8
Há algumas coisas que tenho que considerar aqui, que provavelmente me farão escolher a resposta de @ Paul sobre essa. Estou sozinho ao codificar (por enquanto), portanto, preciso usar uma solução que "eu" me lembre, que usarei repetidamente. Também acho que quanto menos prolixo for o melhor, e criar uma propriedade inversa é muito explícita, facilitando a lembrança, assim como os futuros desenvolvedores (espero, espero), de poder ver rapidamente o que estava fazendo, além de tornar mais fácil para eles me jogarem sob o proverbial ônibus.
Russ
17
Por seus próprios argumentos, IMHO, a solução do conversor é melhor a longo prazo: você só precisa escrever o conversor uma vez e depois pode reutilizá-lo várias vezes. Se você vai para a nova propriedade, você terá que reescrevê-lo em cada classe que precisa dele ...
Thomas Levesque
51
Eu estou usando a mesma abordagem ... mas faz panda saaad ... = (
Max Galkin
27
Comparado a !isso, esse é um código demorado ... As pessoas enviam esforços insanos para separar o que consideram "código" daqueles pobres designers. Extra extra doloroso quando eu sou o codificador e o designer.
Roman Starkov
10
muitas pessoas, inclusive eu, considerariam este um excelente exemplo de excesso de engenharia. Sugiro usar uma propriedade invertida como no post de Paul Alexander abaixo.
Christian Westman
99

Você já considerou uma IsNotReadOnlypropriedade? Se o objeto que está sendo vinculado for um ViewModel em um domínio MVVM, a propriedade adicional fará todo sentido. Se for um modelo direto de entidade, considere a composição e a apresentação de um ViewModel especializado da sua entidade no formulário.

Paul Alexander
fonte
5
Acabei de resolver o mesmo problema usando essa abordagem e concordo que não apenas é mais elegante, mas muito mais sustentável do que usar um conversor.
Alimbada
28
Eu discordo que essa abordagem é melhor que o conversor de valor. Também produz mais código se você precisar de várias instâncias NotProperty.
Thiru
25
MVVM não é sobre não escrever código, é sobre resolver problemas declarativamente. Para esse fim, o conversor é a solução correta.
31412 Jeff
14
O problema com esta solução é que, se você tiver 100 objetos, precisará adicionar uma propriedade IsNotReadOnly a todos os 100 objetos. Essa propriedade teria que ser uma DependencyProperty. Isso adiciona cerca de 10 linhas de código a todos os 100 objetos ou 1000 linhas de código. O conversor é de 20 linhas de código. 1000 linhas ou 20 linhas. Qual você escolheria?
Rhyous
8
Existe um ditado comum para isso: faça uma vez, faça duas vezes e depois automatize. Na dúvida, eu usaria essa resposta na primeira vez que for necessária em um projeto e, se as coisas crescerem, usaria a resposta aceita. Porém, ter o snippet do conversor pré-fabricado pode dificultar o uso.
9239 heltonbiker
71

Com a ligação padrão, você precisa usar conversores com um pouco de vento. Portanto, recomendo que você analise meu projeto CalcBinding , desenvolvido especialmente para resolver esse problema e outros. Com a ligação avançada, você pode escrever expressões com muitas propriedades de origem diretamente no xaml. Digamos, você pode escrever algo como:

<Button IsEnabled="{c:Binding Path=!IsReadOnly}" />

ou

<Button Content="{c:Binding ElementName=grid, Path=ActualWidth+Height}"/>

ou

<Label Content="{c:Binding A+B+C }" />

ou

<Button Visibility="{c:Binding IsChecked, FalseToVisibility=Hidden}" />

onde A, B, C, IsChecked - propriedades do viewModel e ele funcionará corretamente

Alex141
fonte
6
Embora o QuickConverter seja mais poderoso, acho o modo CalcBinding legível - utilizável.
precisa saber é o seguinte
3
Essa é uma ótima ferramenta. Eu gostaria que existisse 5 anos atrás!
jugg1es
Ferramenta brilhante, mas cai em estilos. <Setter.Value><cb:Binding Path="!IsReadOnly" /></Setter.Value>recebe uma 'ligação' não é válido para o erro tempo de compilação Setter.Value'
mcalex
21

Eu recomendaria usar https://quickconverter.codeplex.com/

A inversão de um booleano é tão simples quanto: <Button IsEnabled="{qc:Binding '!$P', P={Binding IsReadOnly}}" />

Isso acelera o tempo normalmente necessário para gravar conversores.

Noxxys
fonte
19
Ao atribuir um -1 a alguém, seria bom explicar o porquê.
Noxxys 20/10/2014
16

Eu queria que meu XAML permanecesse o mais elegante possível, então criei uma classe para agrupar o bool que reside em uma das minhas bibliotecas compartilhadas, os operadores implícitos permitem que a classe seja usada como um bool no code-behind sem problemas

public class InvertableBool
{
    private bool value = false;

    public bool Value { get { return value; } }
    public bool Invert { get { return !value; } }

    public InvertableBool(bool b)
    {
        value = b;
    }

    public static implicit operator InvertableBool(bool b)
    {
        return new InvertableBool(b);
    }

    public static implicit operator bool(InvertableBool b)
    {
        return b.value;
    }

}

As únicas alterações necessárias ao seu projeto são fazer com que a propriedade que você deseja inverter retorne isso em vez de bool

    public InvertableBool IsActive 
    { 
        get 
        { 
            return true; 
        } 
    }

E no XAML postfix a ligação com Value ou Invert

IsEnabled="{Binding IsActive.Value}"

IsEnabled="{Binding IsActive.Invert}"
jevansio
fonte
11
A desvantagem é que você precisaria alterar todo o código que o comparasse com / atribuído a outras boolexpressões / variáveis ​​de tipo, mesmo sem fazer referência ao valor inverso. Em vez disso, eu adicionaria um método de extensão "Não" ao arquivo Boolean Struct.
Tom
11
Doh! Deixa pra lá. Esqueci tinha que ser Propertyvs. Methodpara Binding. Minha declaração "Downside" ainda se aplica. Aliás, o método de extensão "Not" 'booleano' ainda é útil para evitar o "!" Operador que é facilmente esquecido quando (como costuma ser o caso) é incorporado ao lado de caracteres que se parecem com ele (ou seja, um / mais "(" 's e "l" e "I")
Tom
10

Este também funciona para bools anuláveis.

 [ValueConversion(typeof(bool?), typeof(bool))]
public class InverseBooleanConverter : IValueConverter
{
    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (targetType != typeof(bool?))
        {
            throw new InvalidOperationException("The target must be a nullable boolean");
        }
        bool? b = (bool?)value;
        return b.HasValue && !b.Value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return !(value as bool?);
    }

    #endregion
}
Andreas
fonte
4

Adicione mais uma propriedade no seu modelo de exibição, que retornará valor reverso. E ligue isso ao botão. Gostar;

no modelo de exibição:

public bool IsNotReadOnly{get{return !IsReadOnly;}}

no xaml:

IsEnabled="{Binding IsNotReadOnly"}
MMM
fonte
11
Ótima resposta. Uma coisa a acrescentar: é melhor aumentar o evento PropertyChanged para IsNotReadOnly no setter da propriedade IsReadOnly. Com isso, você garantirá que a interface do usuário seja atualizada corretamente.
Muhannad
Essa deve ser a resposta aceita, pois é a mais simples.
gabnaim
2

Não sei se isso é relevante para o XAML, mas no meu aplicativo Windows simples, criei a ligação manualmente e adicionei um manipulador de eventos Format.

public FormMain() {
  InitializeComponent();

  Binding argBinding = new Binding("Enabled", uxCheckBoxArgsNull, "Checked", false, DataSourceUpdateMode.OnPropertyChanged);
  argBinding.Format += new ConvertEventHandler(Binding_Format_BooleanInverse);
  uxTextBoxArgs.DataBindings.Add(argBinding);
}

void Binding_Format_BooleanInverse(object sender, ConvertEventArgs e) {
  bool boolValue = (bool)e.Value;
  e.Value = !boolValue;
}
Simon Dobson
fonte
11
Parece praticamente o mesmo que a abordagem do conversor. Formate Parseeventos nas ligações WinForms são aproximadamente equivalentes ao conversor WPF.
Alejandro
2

Eu tive um problema de inversão, mas uma solução pura.

A motivação foi que o designer XAML mostraria um controle vazio, por exemplo, quando não havia datacontext / no MyValues(itemssource).

Código inicial: oculta o controle quando MyValuesestá vazio. Código aprimorado: mostre o controle quando MyValuesNÃO for nulo ou vazio.

Obviamente, o problema é como expressar '1 ou mais itens', que é o oposto de 0 itens.

<ListBox ItemsSource={Binding MyValues}">
  <ListBox.Style x:Uid="F404D7B2-B7D3-11E7-A5A7-97680265A416">
    <Style TargetType="{x:Type ListBox}">
      <Style.Triggers>
        <DataTrigger Binding="{Binding MyValues.Count}">
          <Setter Property="Visibility" Value="Collapsed"/>
        </DataTrigger>
      </Style.Triggers>
    </Style>
  </ListBox.Style>
</ListBox>

Eu o resolvi adicionando:

<DataTrigger Binding="{Binding MyValues.Count, FallbackValue=0, TargetNullValue=0}">

Ergo definindo o padrão para a encadernação. Claro que isso não funciona para todos os tipos de problemas inversos, mas me ajudou com um código limpo.

EricG
fonte
2

Solution .Net Core Solution 💡

Lida com a situação nula e não lança uma exceção, mas retorna truese nenhum valor for apresentado; caso contrário, pega o booleano inserido e o inverte.

public class BooleanToReverseConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
     => !(bool?) value ?? true;

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
     => !(value as bool?);
}

Xaml

IsEnabled="{Binding IsSuccess Converter={StaticResource BooleanToReverseConverter}}"

App.Xaml Gosto de colocar todas as estatísticas do meu conversor no arquivo app.xaml para não precisar redeclará-las nas janelas / páginas / controles do projeto.

<Application.Resources>
    <converters:BooleanToReverseConverter x:Key="BooleanToReverseConverter"/>
    <local:FauxVM x:Key="VM" />
</Application.Resources>

Para ser claro, converters:é o espaço para nome da implementação real da classe ( xmlns:converters="clr-namespace:ProvingGround.Converters").

ΩmegaMan
fonte
1

Após a resposta de @ Paul, escrevi o seguinte no ViewModel:

public bool ShowAtView { get; set; }
public bool InvShowAtView { get { return !ShowAtView; } }

Espero que ter um trecho aqui ajude alguém, provavelmente novato como eu.
E se houver algum erro, avise-me!

BTW, eu também concordo com o comentário @heltonbiker - é definitivamente a abordagem correta apenas se você não precisar usá-la mais de três vezes ...

Ofaim
fonte
2
Não sendo uma propriedade completa e sem um "OnPropertyChanged", isso não funcionará. 1 ou 2 respostas é o que estou usando, dependendo do caso. A menos que você esteja usando uma estrutura como o Prism, na qual o frameowkr sabe quando atualizar as propriedades "referidas". Então é um lance entre usando algo parecido com o que você sugeriu (mas com a propriedade plena), e resposta 1
Oyiwai
1

Eu fiz algo muito parecido. Criei minha propriedade nos bastidores que permitia a seleção de uma caixa de combinação SOMENTE se ela terminasse de pesquisar dados. Quando minha janela aparece pela primeira vez, ele lança um comando carregado assíncrono, mas não quero que o usuário clique na caixa de combinação enquanto ainda carrega dados (estaria vazio e preenchido). Então, por padrão, a propriedade é false, então eu retorno o inverso no getter. Então, quando estou pesquisando, defino a propriedade como true e retorne para false quando concluir.

private bool _isSearching;
public bool IsSearching
{
    get { return !_isSearching; }
    set
    {
        if(_isSearching != value)
        {
            _isSearching = value;
            OnPropertyChanged("IsSearching");
        }
    }
}

public CityViewModel()
{
    LoadedCommand = new DelegateCommandAsync(LoadCity, LoadCanExecute);
}

private async Task LoadCity(object pArg)
{
    IsSearching = true;

    //**Do your searching task here**

    IsSearching = false;
}

private bool LoadCanExecute(object pArg)
{
    return IsSearching;
}

Em seguida, para a caixa de combinação, posso vinculá-lo diretamente ao IsSearching:

<ComboBox ItemsSource="{Binding Cities}" IsEnabled="{Binding IsSearching}" DisplayMemberPath="City" />
GregN
fonte
0

Eu uso uma abordagem semelhante como @Ofaim

private bool jobSaved = true;
private bool JobSaved    
{ 
    get => jobSaved; 
    set
    {
        if (value == jobSaved) return;
        jobSaved = value;

        OnPropertyChanged();
        OnPropertyChanged("EnableSaveButton");
    }
}

public bool EnableSaveButton => !jobSaved;
soulflyman
fonte