Atualizar a interface do usuário quando Collections sofrer alteração

Concluído

Nesta lição, o usuário selecionará cores favoritas. As cores disponíveis são listadas em uma lista suspensa (um controle ComboBox). O usuário escolhe uma cor e a adiciona aos favoritos pressionando um botão. As cores favoritas serão exibidas abaixo. A escolha de uma cor favorita também exibirá um botão que permitirá ao usuário remover a cor escolhida de seus favoritos.

Screenshot of sample databinding app running and displaying favorite colors.

Primeiro, você adicionará o recurso de escolher uma cor da coleção LotsOfColors e a adicionará à lista de cores favoritas.

1. Criar a propriedade SelectedColor

Vamos começar com o código e depois trataremos da interface do usuário.

Precisamos de uma forma de determinar qual item (ou seja, instância da classe ColorDescriptor) o usuário escolheu no menu suspenso. O controle ComboBox tem uma propriedade SelectedItem, que obtém e define o item escolhido no momento. Assim, poderemos associar essa propriedade a uma propriedade do tipo ColorDescriptor em nosso código.

Abra o ColorListLogic.cs e adicione o seguinte código:

private ColorDescriptor _selectedColor;

public ColorDescriptor SelectedColor
{
    get => _selectedColor;
    set => Set(ref _selectedColor, value);
}

Você já deve conhecer bem esse padrão. Ele representa uma propriedade padrão decorada com o mecanismo INotifyPropertyChanged com a ajuda da classe base ObservableObject.

2. Criar a lista FavoriteColors

A lista FavoriteColors armazenará as cores que o usuário escolheu como favoritas. É apenas uma propriedade simples.

public List<ColorDescriptor> FavoriteColors { get; } = 
    new List<ColorDescriptor>();

3. Adicionar a cor escolhida aos Favoritos

A adição da cor escolhida aos Favoritos ocorrerá no método AddSelectedColorToFavorites.

public void AddSelectedColorToFavorites()
{
    FavoriteColors.Add(SelectedColor);
}

Pressupõe-se que quando esse método é chamado, SelectedColor é preenchido com a cor que deve ser adicionada à lista de favoritos.

Com isso, concluímos o código (por enquanto). Vamos voltar nossa atenção ao XAML.

4. Alterar a ListBox para uma ComboBox

Como queremos que a lista completa de cores seja mostrada em um menu suspenso (que é um controle ComboBox), precisamos alterar o XAML. Felizmente, ListBox e ComboBox descendem do controle ItemsControl e funcionam de forma semelhante, apesar da diferença na aparência e no comportamento. Tudo o que precisamos fazer é substituir ListBox por ComboBox no arquivo ColorList.xaml. Você pode usar o comando Editar>Encontrar e Substituir>Substituição rápida (Ctrl+H) para realizar a substituição.

Screenshot of Visual Studio showing the Quick Replace command.

Se você executar rapidamente o aplicativo agora, verá que ListBox foi substituído por ComboBox, mas as cores ainda são exibidas usando o mesmo modelo.

Screenshot of favorite colors app showing the color selection combo box.

5. Extrair o modelo para um recurso

Em relação ao modelo, você precisará reutilizá-lo na lista de cores favoritas posteriormente. É uma boa ideia armazenar o modelo em um só lugar, para que o XAML seja mais legível. Mais importante, isso garante que as alterações no modelo afetem todas as instâncias. Vamos extrair o modelo para um recurso.

Qualquer FrameworkElement pode ter sua lista de recursos. Optamos por tornar nosso modelo global para esta página, portanto, vamos adicionar uma marca <Page.Reources> acima do elemento <Grid>. Em seguida, mova toda a marca <DataTemplate> e o conteúdo dentro dela.

<Page.Resources>
    <DataTemplate x:DataType="local:ColorDescriptor">
        <StackPanel Orientation="Horizontal">
            <Rectangle Width="30" 
                       Height="30">
                <Rectangle.Fill>
                    <SolidColorBrush Color="{x:Bind Color}"/>
                </Rectangle.Fill>
            </Rectangle>
            <TextBlock Text="{x:Bind Name}" 
                       Margin="20, 10, 0, 10"/>
        </StackPanel>
    </DataTemplate>
</Page.Resources>

O Visual Studio avisa-o que os objetos dentro da marca <Page.Resources> (que é um IDictionary) devem ter um atributo Key, então adicione-o ao <DataTemplate>.

<DataTemplate x:Key="ColorTemplate" 
              x:DataType="local:ColorDescriptor">

Essa chave permite que façamos referência a este modelo de outro lugar na página, como o ComboBox.ItemTemplate, que perdeu seu conteúdo. Para que a ComboBox use o recurso ColorDescriptor, você pode remover a marca <ComboBox.ItemTemplate> e usá-la como um atributo dentro da própria marca <ComboBox>. Toda a marca <ComboBox> tem a seguinte aparência:

<ComboBox ItemsSource="{x:Bind Logic.LotsOfColors}" 
          Margin="20" 
          Width="200"
          HorizontalAlignment="Left" 
          VerticalAlignment="Top"
          ItemTemplate="{StaticResource ColorTemplate}"/>

Você pode executar o aplicativo novamente apenas para verificar se o modelo de item funciona.

6. Compilar o restante da interface do usuário

A interface do usuário é simples. Todos os controles (o menu suspenso, o botão Adicionar a Favoritos, a lista de favoritos (com o texto do cabeçalho) e o botão Remover dos Favoritos) são colocados em um único StackPanel vertical. Substitua todo o conteúdo do elemento <Grid> pelo seguinte:

<StackPanel>
    <ComboBox ItemsSource="{x:Bind Logic.LotsOfColors}" 
              Margin="20, 20, 20, 0" 
              Width="200"
              HorizontalAlignment="Left" 
              VerticalAlignment="Top"
              ItemTemplate="{StaticResource ColorTemplate}"
              SelectedItem="{x:Bind Logic.SelectedColor, Mode=TwoWay}" 
              />

    <Button Margin="20" 
            Click="{x:Bind Logic.AddSelectedColorToFavorites}">Add to Favorites</Button>
    <TextBlock FontSize="25" 
               Margin="20, 20, 20, 0">Favorite colors</TextBlock>

    <ListBox ItemsSource="{x:Bind Logic.FavoriteColors}"
             ItemTemplate="{StaticResource ColorTemplate}"
             Margin="20, 20, 20, 0"/>

    <Button Margin="20">Remove from Favorites</Button>
</StackPanel>

Há uma associação TwoWay entre o item atualmente selecionado na ComboBox e a propriedade SelectedColor da classe ColorListLogic.

7. Execute o aplicativo

Execute o aplicativo agora, escolha uma cor em ComboBox e selecione o botão Adicionar aos favoritos. Nada acontecerá. Adicionar um ponto de interrupção no final do método AddSelectedColorToFavorites na classe ColorListLogic mostra que o código funciona. A cor selecionada é adicionada à lista FavoriteColors.

A interface do usuário não reflete as alterações nesta List<ColorDescriptor> porque precisa ser notificada quando a coleção é alterada. No caso das listas, isso é feito pela interface System.Collections.Specialized.INotifyCollectionChanged. Felizmente, isso não precisa ser implementado. A classe System.Collections.ObjectModel.ObservableCollection<T> já possui tudo aquilo de que precisamos.

Para fazer nosso aplicativo funcionar, tudo que precisamos fazer é usar a classe ObservableCollection<T> em vez da classe List<T> na propriedade FavoriteColors.

public ObservableCollection<ColorDescriptor> FavoriteColors { get; } = 
    new ObservableCollection<ColorDescriptor>();

Se você executar o aplicativo agora, a escolha das cores no menu suspenso e o botão Adicionar a Favoritos já funcionarão corretamente. As cores favoritas escolhidas serão refletidas na ListBox. Ótimo!

Screenshot of sample app showing favorite colors added to list.

8. Evite adicionar itens vazios

Quando você inicia o aplicativo, não há cores marcadas na lista suspensa. Se você marcar o botão Adicionar a Favoritos agora, nulls serão adicionados à lista. É um bug e vamos corrigi-lo!

Poderíamos adicionar uma verificação de null ao método AddSelectedColorToFavorites, mas isso não impediria que o botão Adicionar a Favoritos aparecesse quando não fosse funcional. Em vez disso, vamos garantir que sempre haja um item escolhido no menu suspenso. Como a propriedade SelectedItem do menu suspenso é associada de duas maneiras à propriedade SelectedColor no código, vamos apenas inicializá-la com um valor válido no início. Adicione a linha a seguir no final do construtor ColorListLogic:

SelectedColor = LotsOfColors[0];

Isso garante que o primeiro item da lista LotsOfColors seja selecionado quando o aplicativo for iniciado. O usuário não será capaz de adicionar um null à coleção FavoriteColors.

9. Remover cores favoritas

A próxima etapa é adicionar a capacidade de remover cores favoritas da ListBox. Isso acontecerá quando o usuário escolher um item na ListBox e selecionar o botão Remover dos Favoritos.

Com um procedimento similar ao aplicado à ComboBox, podemos rastrear o item que o usuário escolheu na ListBox usando a propriedade SelectedItem. Podemos associar isso a uma propriedade no código. Adicione esse código à classe ColorListLogic.

private ColorDescriptor _selectedFavoriteColor;

public ColorDescriptor SelectedFavoriteColor
{
    get => _selectedFavoriteColor;
    set
    {
        Set(ref _selectedFavoriteColor, value);
        RaisePropertyChanged(nameof(IsRemoveFavoriteColorButtonVisible));
    }
}

public bool IsRemoveFavoriteColorButtonVisible => SelectedFavoriteColor != null;

O código anterior também inclui uma propriedade booliana para controlar se o botão para remover itens da lista de favoritos deve ficar visível. Qualquer alteração realizada em SelectedFavoriteColor fará com que a interface do usuário consulte essa propriedade e aja em conformidade.

Para realmente executar a remoção da cor da lista de Favoritos, precisamos gravar mais um método.

public void RemoveFavoriteColor()
{
    FavoriteColors.Remove(SelectedFavoriteColor);
}

Para conectar o botão no XAML, abra ColorList.xaml e altere o XAML do botão Remover dos Favoritos. Faça isso para que ele inclua a associação Visibility, bem como a associação Click.

<Button Margin="20" 
        Visibility="{x:Bind Logic.IsRemoveFavoriteColorButtonVisible, Mode=OneWay}"
        Click="{x:Bind Logic.RemoveFavoriteColor}">Remove from Favorites</Button>

Falta apenas associar o SelectedItem do ListBox à propriedade Logic.SelectedFavoriteColor. Adicione o atributo SelectedItem ao ListBox no XAML.

<ListBox SelectedItem="{x:Bind Logic.SelectedFavoriteColor, Mode=TwoWay}"... >

Execute o aplicativo agora e verifique se consegue adicionar cores à lista de cores favoritas e removê-las. Observe como o botão Remover dos Favoritos aparece e desaparece de acordo com a escolha de uma cor favorita que pode ser removida.

Resumo

Esta lição ilustrou como você pode adquirir e configurar o item escolhido em um controle ComboBox ou ListBox ao associar suas propriedades SelectedItem a uma propriedade C#. Você também viu que o uso de ObservableCollection no código faz com que a interface do usuário atualize automaticamente o conteúdo dos controles que exibem vários itens.

Nesta lição, o usuário selecionará as cores favoritas dele. As cores disponíveis são listadas em uma lista suspensa (um controle ComboBox). O usuário escolhe uma cor e a adiciona aos favoritos pressionando um botão. As cores favoritas são exibidas abaixo da lista completa. A escolha de uma cor favorita também exibirá um botão que permitirá ao usuário remover a cor escolhida de seus favoritos.

Screenshot of sample app showing selected favorite color with remove button available.

Primeiro, vamos adicionar a capacidade de escolher uma cor da coleção LotsOfColors e adicioná-la à lista de cores favoritas.

1. Criar a propriedade SelectedColor

Vamos começar com o código e depois trataremos das alterações na interface do usuário.

Precisamos de uma forma de determinar qual item (ou seja, instância da classe ColorDescriptor) o usuário escolheu no menu suspenso. O controle ComboBox tem uma propriedade SelectedItem, que obtém e define o item escolhido no momento. Assim, poderemos associar essa propriedade a uma propriedade do tipo ColorDescriptor em nosso código.

Abra ColorListDataContext.cs e adicione o seguinte código:

private ColorDescriptor? _selectedColor;

public ColorDescriptor? SelectedColor
{
    get => _selectedColor;
    set => Set(ref _selectedColor, value);
}

Você já deve conhecer bem esse padrão. Ele representa uma propriedade padrão que aproveita o mecanismo INotifyPropertyChanged com a ajuda da classe base ObservableObject.

2. Criar a lista FavoriteColors

A lista FavoriteColors armazenará as cores que o usuário escolheu como favoritas. É apenas uma propriedade simples.

public List<ColorDescriptor> FavoriteColors { get; } = 
    new List<ColorDescriptor>();

3. Adicionar a cor escolhida aos Favoritos

A adição da cor escolhida aos Favoritos ocorrerá no método AddSelectedColorToFavorites. Por precaução, confira se a propriedade SelectedColor é null. Se for, retorne por meio do método. Caso contrário, adicione a cor selecionada à lista FavoriteColors.

public void AddSelectedColorToFavorites()
{
    if (SelectedColor == null) return;
    FavoriteColors.Add(SelectedColor);
}

Quando esse método for chamado, SelectedColor deverá ser preenchido com a cor que está sendo adicionada à lista de favoritos, mas é melhor não fazer suposições.

Com isso, concluímos o código (por enquanto). Vamos voltar nossa atenção ao XAML.

4. Alterar a ListBox para uma ComboBox

Como queremos que a lista completa de cores seja mostrada em um menu suspenso (que é um controle ComboBox), precisamos alterar o XAML. Felizmente, ListBox e ComboBox descendem do controle ItemsControl e funcionam de forma semelhante, apesar da grande diferença na aparência e no comportamento. Tudo o que precisamos fazer é substituir ListBox por ComboBox no arquivo ColorList.xaml. Você pode usar o comando Editar>Encontrar e Substituir>Substituição rápida (Ctrl+H) para realizar a substituição.

Repeat screenshot of Visual Studio showing Quick Replace command.

Se você executar rapidamente o aplicativo agora, verá que ListBox foi substituído por ComboBox, mas as cores ainda são exibidas usando o mesmo modelo.

Screenshot of sample app showing color list in a ComboBox.

5. Extrair o modelo para um recurso

Em relação ao modelo, você precisará reutilizá-lo na lista de cores favoritas posteriormente. É uma boa ideia armazenar o modelo em um só lugar, para que o XAML seja mais legível. Mais importante, isso garante que as alterações no modelo afetem todas as instâncias. Vamos extrair o modelo para um recurso.

Qualquer FrameworkElement pode ter sua lista de recursos. Optamos por tornar nosso modelo global para a Window inteira, portanto, vamos adicionar uma marca <Window.Reources> acima do elemento <Grid>. Em seguida, mova toda a marca <DataTemplate> e o conteúdo dentro dela.

<Window.Resources>
    <DataTemplate x:Key="ColorTemplate">
        <StackPanel Orientation="Horizontal">
            <Rectangle Width="80" 
                       Height="20">
                <Rectangle.Fill>
                    <SolidColorBrush Color="{Binding Color}"/>
                </Rectangle.Fill>
            </Rectangle>
            <TextBlock Text="{Binding Name}" 
                       Margin="20, 10, 0, 10"/>
        </StackPanel>
    </DataTemplate>
    <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</Window.Resources>

O <Window.Resources> é um dicionário, portanto, cada entrada também deverá ter uma chave. Nós o adicionamos ao <DataTemplate>.

<DataTemplate x:Key="ColorTemplate">

Essa chave permitirá que façamos referência a este modelo de outro lugar dentro do Window, como o ComboBox.ItemTemplate, que perdeu seu conteúdo. Para que a ComboBox use o recurso ColorDescriptor, você pode remover a marca <ComboBox.ItemTemplate> e usá-la como um atributo dentro da própria marca <ComboBox>. Toda a marca <ComboBox> tem a seguinte aparência:

<ComboBox ItemsSource="{x:Bind Logic.LotsOfColors}" 
          Margin="20" 
          Width="200"
          HorizontalAlignment="Left" 
          VerticalAlignment="Top"
          ItemTemplate="{StaticResource ColorTemplate}"/>

Você pode executar o aplicativo novamente apenas para verificar se o modelo de item funciona.

6. Compilar o restante da interface do usuário

A interface do usuário é simples. Todos os controles (o menu suspenso, o botão Adicionar a Favoritos, a lista de favoritos (com o texto do cabeçalho) e o botão Remover dos Favoritos) ficam aninhados em um único StackPanel vertical. Substitua todo o elemento <Grid> pelo seguinte:

<StackPanel>
    <ComboBox ItemsSource="{Binding LotsOfColors}" 
              Margin="20, 20, 20, 0" 
              Width="200"
              HorizontalAlignment="Left" 
              VerticalAlignment="Top"
              ItemTemplate="{StaticResource ColorTemplate}"
              SelectedItem="{Binding SelectedColor, Mode=TwoWay}" />

    <Button 
        Margin="20" 
        HorizontalAlignment="Left">Add to Favorites</Button>

    <TextBlock
            FontSize="25" 
            Margin="20, 20, 20, 0">Favorite colors</TextBlock>

    <ListBox ItemsSource="{Binding FavoriteColors}"
             ItemTemplate="{StaticResource ColorTemplate}"
             Margin="20, 20, 20, 0"
             HorizontalAlignment="Left"/>

    <Button Margin="20" 
            HorizontalAlignment="Left">Remove from Favorites</Button>

</StackPanel>

Há uma associação TwoWay entre o item atualmente selecionado na ComboBox e a propriedade SelectedColor da classe ColorListDataContext.

7. Criar um manipulador de cliques para o botão Add to Favorites

Já implementamos a lógica para adicionar a cor escolhida à Lista FavoriteColors no método AddSelectedColorToFavorites. No entanto, é necessário invocar esse método quando o usuário clica no botão.

Clique duas vezes no botão Add to Favorites no designer visual no Visual Studio. Isso cria um método Button_Click no code-behind e uma referência a ele no evento Click no XAML:

<Button ... Click="Button_Click">Add to Favorites</Button>

Renomeie o método Button_Click como AddToFavorites_Click no code-behind e no XAML. Para fazer isso, renomeie o método no código e, usando o ícone de engrenagem, escolha "Renomear Button_Click como AddToFavorites_Click". Essa ação também cuidará da alteração do nome do método no arquivo XAML.

Screenshot of Visual Studio quick action option to rename button click method..

Adicione uma propriedade de conveniência ao início da classe ColorList para facilitar o acesso ao ColorListDataContext:

private ColorListDataContext DC => (ColorListDataContext)DataContext;

Em seguida, no método AddToFavorites_Click, é preciso invocar a lógica previamente gravada na classe ColorListDataContext:

private void AddToFavorites_Click(object sender, RoutedEventArgs e)
{
    DC.AddSelectedColorToFavorites();
}

8. Execute o aplicativo

Execute o aplicativo agora, escolha uma cor em ComboBox e selecione o botão Adicionar aos favoritos. Nada acontecerá. Adicionar um ponto de interrupção no final do método AddSelectedColorToFavorites na classe ColorListDataContext mostra que o código funciona. A cor selecionada é adicionada à lista FavoriteColors.

A interface do usuário não reflete as alterações nesta List<ColorDescriptor> porque precisa ser notificada quando a coleção é alterada. No caso das listas, isso é feito pela interface System.Collections.Specialized.INotifyCollectionChanged. Felizmente, isso não precisa ser implementado. A classe System.Collections.ObjectModel.ObservableCollection<T> já possui tudo aquilo de que precisamos.

Para fazer nosso aplicativo funcionar, tudo que precisamos fazer é usar a classe ObservableCollection<T> em vez da classe List<T> na propriedade FavoriteColors.

public ObservableCollection<ColorDescriptor> FavoriteColors { get; } = 
    new ObservableCollection<ColorDescriptor>();

Se você executar o aplicativo agora, a escolha das cores no menu suspenso e o botão Adicionar a Favoritos já funcionarão corretamente. As cores favoritas escolhidas serão refletidas na ListBox. Ótimo!

Screenshot of sample app showing selected color added to favorites.

9. Evite adicionar itens vazios

Quando você inicia o aplicativo, não há cores marcadas na lista suspensa. No momento, temos uma verificação null no método AddSelectedColorToFavorites para impedir que itens "nulos" sejam adicionados à lista ao selecionar o botão Adicionar aos Favoritos. Vamos alterar isso para garantir que sempre haja uma cor selecionada na lista suspensa.

A verificação null não impede que o botão Adicionar a Favoritos apareça quando não está funcional. Portanto, vamos garantir que sempre haja um item selecionado. Como a propriedade SelectedItem do menu suspenso é associada de duas maneiras à propriedade SelectedColor no código, vamos apenas inicializá-la com um valor válido no início. Adicione a linha a seguir no final do construtor ColorListDataContext:

SelectedColor = LotsOfColors[0];

Isso garante que o primeiro item da lista LotsOfColors seja selecionado quando o aplicativo for iniciado. O usuário não será capaz de adicionar um null à coleção FavoriteColors.

10. Remover cores favoritas

A próxima etapa é adicionar a capacidade de remover cores favoritas da ListBox. Isso acontecerá quando o usuário escolher um item na ListBox e selecionar o botão Remover dos Favoritos.

Com um procedimento similar ao aplicado à ComboBox, podemos rastrear o item que o usuário escolheu na ListBox usando a propriedade SelectedItem. Podemos associar isso a uma propriedade no código. Adicione a nova propriedade à classe ColorListDataContext da seguinte maneira:

private ColorDescriptor? _selectedFavoriteColor;

public ColorDescriptor? SelectedFavoriteColor
{
    get => _selectedFavoriteColor;
    set
    {
        Set(ref _selectedFavoriteColor, value);
        RaisePropertyChanged(nameof(IsRemoveFavoriteEnabled));
    }
}

public bool IsRemoveFavoriteEnabled => SelectedFavoriteColor != null;

O código anterior também inclui uma propriedade booliana para controlar se o botão para remover itens da lista de favoritos deve ficar visível. Qualquer alteração realizada em SelectedFavoriteColor fará com que a interface do usuário consulte essa propriedade e aja em conformidade.

Para realmente executar a remoção da cor da lista de Favoritos, precisamos gravar mais um método.

public void RemoveFavoriteColor()
{
    if (SelectedFavoriteColor == null) return;
    FavoriteColors.Remove(SelectedFavoriteColor);
}

Associe o SelectedItem do ListBox à propriedade SelectedFavoriteColor. Adicione o atributo SelectedItem ao ListBox em CodeList.xaml.

<ListBox SelectedItem="{Binding SelectedFavoriteColor, Mode=TwoWay}"... >

Para conectar o botão Remover dos Favoritos altere o XAML do botão Remover dos Favoritos para que ele inclua a associação IsEnabled e o manipulador de eventos Click.

<Button Margin="20" 
        HorizontalAlignment="Left"
        Click="RemoveFromFavorites_Click"
        IsEnabled="{Binding IsRemoveFavoriteEnabled}">Remove from Favorites</Button>

Também precisamos adicionar o método RemoveFromFavorites_Click ao ColorList.xaml.cs para chamar RemoveFromFavoriteColor em nossa lógica.

private void RemoveFromFavorites_Click(object sender, RoutedEventArgs e)
{
    DC.RemoveFavoriteColor();
}

Execute o aplicativo agora e verifique se consegue adicionar cores à lista de cores favoritas e removê-las. Observe como o botão Remover dos Favoritos fica habilitado e desabilitado de acordo com a escolha de uma cor favorita que pode ser removida.

Como exercício, tente ocultar toda a parte Cores Favoritas da interface do usuário quando a coleção FavoriteColors estiver vazia. Dica: use um StackPanel para agrupar os controles afetados e associar a Visibilidade de StackPanel para uma propriedade na classe ColorListDataContext. Sempre que uma cor favorita for adicionada ou removida, notifique a interface do usuário sobre as alterações nessa propriedade.

Resumo

Esta lição ilustrou como você pode adquirir e configurar o item escolhido em um controle ComboBox ou ListBox ao associar suas propriedades SelectedItem a uma propriedade C#. Você também viu que o uso de ObservableCollection no código faz com que a interface do usuário atualize automaticamente o conteúdo dos controles que exibem vários itens.