Search code examples
c#wpflistboxselecteditemlistboxitem

How to persistently change color of ListBox SelectedItem after selecting


I have a listbox that loads it's items with Foreground color set to red. What I'd like to do is: upon selecting an item with the mouse, change the foreground color of SelectedItem to black, but make the change persistent so that after deselecting the item, color remains black. Incidentally I want to implement this as a way of showing 'read items' to the user.

Essentially I want something like an implementation of the common property trigger like the code below, but not have the style revert after deselection. I've played around with event triggers as well without much luck.

        <ListBox.ItemContainerStyle>
            <Style TargetType="ListBoxItem">
                <Style.Triggers>
                    <Trigger Property="IsSelected" Value="True" >
                        <Setter Property="Foreground" Value="Black" />   //make this persist after deselection
                    </Trigger>
                </Style.Triggers>
            </Style>                
        </ListBox.ItemContainerStyle>

Thanks in advance!


Solution

  • You could animate the Foreground property:

    <ListBox>
      <ListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem">
          <Setter Property="Foreground" Value="Red" />
    
          <Style.Triggers>
            <Trigger Property="IsSelected" Value="True">
              <Trigger.EnterActions>
                <BeginStoryboard>
                  <Storyboard>
                    <ColorAnimation Storyboard.TargetProperty="(ListBoxItem.Foreground).(SolidColorBrush.Color)"
                                    To="Black" />
                  </Storyboard>
                </BeginStoryboard>
              </Trigger.EnterActions>
            </Trigger>
          </Style.Triggers>
        </Style>
      </ListBox.ItemContainerStyle>
    </ListBox>
    

    The downside of this simple approach is that the information is not stored somewhere. This is pure visualization without any data backing. In order to persist the information, so that restarting the application shows the same previous state, you should introduce a dedicated property to your data model e.g IsMarkedAsRead.

    Depending on your requirements, you can override the ListBoxItem.Template and bind ToggleButton.IsChecked to IsMarkedAsRead or use a Button which uses a ICommand to set the IsMarkedAsRead property. There are many solutions e.g. implementing an Attached Behavior.

    The following examples overrides the ListBoxItem.Template to turn the ListBoxItem into a Button. Now when the item is clicked the IsMarkedAsRead property of the data model is set to true:

    Data model
    (See Microsoft Docs: Patterns - WPF Apps With The Model-View-ViewModel Design Pattern for an implementation example of the RelayCommand.)

    public class Notification : INotifyPropertyChanged
    {    
      public string Text { get; set; }
      public ICommand MarkAsReadCommand => new RelayCommand(() => this.IsMarkedAsRead = true);
      public ICommand MarkAsUnreadCommand => new RelayCommand(() => this.IsMarkedAsRead = false);
      private bool isMarkedAsRead;
    
      public bool IsMarkedAsRead
      {
        get => this.isMarkedAsRead;
        set
        {
          this.isMarkedAsRead = value;
          OnPropertyChanged();
        }
      }
    
      #region INotifyPropertyChanged
    
      public event PropertyChangedEventHandler PropertyChanged;
    
      protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
      {
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
      }
    
      #endregion
    }
    

    ListBox

    <ListBox ItemsSource="{Binding Notifications}">
      <ListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem">
          <Setter Property="Template">
            <Setter.Value>
              <ControlTemplate TargetType="ListBoxItem">
                <Border Background="{TemplateBinding Background}">
                  <Button x:Name="ContentPresenter"
                          ContentTemplate="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=ListBox}, Path=ItemTemplate}"
                          Content="{TemplateBinding Content}"
                          Command="{Binding MarkAsReadCommand}"
                          Foreground="Red">
                    <Button.Template>
                      <ControlTemplate TargetType="Button">
                        <Border>
                          <ContentPresenter />
                        </Border>
                      </ControlTemplate>
                    </Button.Template>
                  </Button>
                </Border>
                <ControlTemplate.Triggers>
                  <DataTrigger Binding="{Binding IsMarkedAsRead}" Value="True">
                    <Setter TargetName="ContentPresenter" Property="Foreground" Value="Green" />
                  </DataTrigger>
                </ControlTemplate.Triggers>
              </ControlTemplate>
            </Setter.Value>
          </Setter>
        </Style>
      </ListBox.ItemContainerStyle>
    
      <ListBox.ItemTemplate>
        <DataTemplate DataType="{x:Type Notification}">
          <TextBlock Text="{Binding Text}"/>
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>