0
votes

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!

2

2 Answers

1
votes

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>
0
votes

Thanks a lot @BionicCode for the comprehensive answer. I ended up going with another solution which may or may not be good convention; I am a hobbyist.

Firstly, I don't need databacking / persistence.

Concerning the data model solution and overriding ListBoxItem.Template, I am using a prededfined class 'SyndicationItem' as the data class (my app is Rss Reader). To implement your datamodel solution I guess I could hack an unused SyndicationItem property, or use SyndicationItem inheritance for a custom class (I'm guessing this is the most professional way?)

My complete data model is as follows:

ObservableCollection >>> CollectionViewSource >>> ListBox.

Anyway I ended up using some simple code behind which wasn't so simple at the time:

First the XAML:

 <Window.Resources>
        <CollectionViewSource x:Key="fooCollectionViewSource" Source="{Binding fooObservableCollection}" >
            <CollectionViewSource.SortDescriptions>
                <scm:SortDescription PropertyName="PublishDate" Direction="Descending" />
            </CollectionViewSource.SortDescriptions>
        </CollectionViewSource>
        <Style x:Key="DeselectedTemplate" TargetType="{x:Type ListBoxItem}">
            <Setter Property="Foreground" Value="Gray" />
        </Style>
    </Window.Resources>

<ListBox x:Name="LB1" ItemsSource="{Binding Source={StaticResource fooCollectionViewSource}}"   HorizontalContentAlignment="Stretch"  Margin="0,0,0,121" ScrollViewer.HorizontalScrollBarVisibility="Disabled" >

    <ListBox.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"  />
                    <ColumnDefinition Width="80" />
                </Grid.ColumnDefinitions>

                <TextBlock MouseDown="TextBlock_MouseDown"  Grid.Column="0" Text="{Binding Path=Title.Text}" TextWrapping="Wrap" FontWeight="Bold"  />
                <TextBlock Grid.Column="1" HorizontalAlignment="Right" TextAlignment="Center" FontSize="11" FontWeight="SemiBold" 
                    Text="{Binding Path=PublishDate.LocalDateTime, StringFormat='{}{0:d MMM,  HH:mm}'}"/>
            </Grid>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Now the code behind:

Solution 1: this applies a new style when listboxitem is deselected. Not used anymore so the LB1_SelectionChanged event is not present in the XAML.

        private void LB1_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (e.RemovedItems.Count != 0)

            {
                foreach (var lbItem in e.RemovedItems)
                {                                  
                    //get reference to source listbox item.  This was a pain.
                    int intDeselectedItem = LB1.Items.IndexOf(lbItem);
                    ListBoxItem lbi = (ListBoxItem)LB1.ItemContainerGenerator.ContainerFromIndex(intDeselectedItem);

                    /*apply style. Initially, instead of applying a style, I used mylistboxitem.Foreground = Brushes.Gray to set the text color.
                    Howver I noticed that if I scrolled the ListBox to the bottom, the text color would revert to the XAML default style in my XAML.
                    I assume this is because of refreshes / redraws (whichever the correct term).  Applying a new style resolved.*/ 
                    Style style = this.FindResource("DeselectedTemplate") as Style;
                    lbi.Style = style;
                }
            }
        }

Solution 2: The one I went with. Occurs on SelectedItem = true, same effect as your first suggestion.

       private void TextBlock_MouseDown(object sender, MouseButtonEventArgs e)
        {
            TextBlock tb = e.Source as TextBlock;
            tb.Foreground = Brushes.Gray;

        }