选择后如何持续更改ListBox SelectedItem的颜色

How to persistently change color of ListBox SelectedItem after selecting

我有一个列表框,用于加载前景色设置为红色的项目。我想做的是:用鼠标选择一个项目后,将 SelectedItem 的前景色更改为黑色,但要使更改持久化,以便在取消选择该项目后,颜色保持黑色。顺便说一句,我想将此实现为向用户显示 'read items' 的一种方式。

本质上,我想要类似于下面代码的常见 属性 触发器的实现,但在取消选择后不恢复样式。我也玩过事件触发器,但运气不佳。

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

提前致谢!

您可以为 Foreground 属性:

设置动画
<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>

这种简单方法的缺点是信息没有存储在某个地方。这是没有任何数据支持的纯可视化。为了保留信息,以便重新启动应用程序显示相同的先前状态,您应该为您的数据模型引入一个专用的 属性,例如 IsMarkedAsRead

根据您的要求,您可以覆盖 ListBoxItem.Template 并将 ToggleButton.IsChecked 绑定到 IsMarkedAsRead 或使用 Button,它使用 ICommand 来设置 IsMarkedAsRead 属性。有很多解决方案,例如实施附加行为。

以下示例覆盖 ListBoxItem.Template 以将 ListBoxItem 变成 Button。现在,当单击该项目时,数据模型的 IsMarkedAsRead 属性 设置为 true:

数据模型
(有关 RelayCommand 的实施示例,请参阅 Microsoft Docs: Patterns - WPF Apps With The Model-View-ViewModel Design Pattern。)

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

非常感谢@BionicCode 的全面回答。我最终采用了另一种解决方案,这可能是也可能不是好的约定;我是一个爱好者。

首先,我不需要数据备份/持久性。

关于数据模型解决方案和覆盖 ListBoxItem.Template,我使用预定义的 class 'SyndicationItem' 作为数据 class(我的应用程序是 Rss Reader).为了实现您的数据模型解决方案,我想我可以破解一个未使用的 SyndicationItem 属性,或者使用 SyndicationItem 继承来自定义 class(我猜这是最专业的方法?)

我的完整数据模型如下:

ObservableCollection >>> CollectionViewSource >>> ListBox.

无论如何,我最终使用了一些当时并不那么简单的简单代码:

首先是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>

下面是代码:

解决方案 1:取消选择 listboxitem 时应用新样式。不再使用,因此 LB1_SelectionChanged 事件不存在于 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;
                }
            }
        }

解决方案 2:我选择的那个。在 SelectedItem = true 时发生,效果与您的第一个建议相同。

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

        }