Search code examples
c#wpfxamlcaliburn.micro

binding a property (datatrigger) with simple trigger IsMouseOver on ListBoxItem


i have looked at multiple posts on SO and i am using one of them validated (one solution) for my solution (i have adapted to my context), but when i use the same thing, its not good:

the line <Condition Binding="{Binding Path=IsMouseOver, RelativeSource={RelativeSource Self}}" Value="True"/> seems to be the problem because when i comment this line, all blank line becomes pinky, but i dont see why, the logic seems good..

View:

<UserControl x:Class="FTPserver.Views.FilesView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:cal="http://www.caliburnproject.org"
             xmlns:local="clr-namespace:FTPserver.Views"
             xmlns:converters="clr-namespace:FTPserver.Converters"
             xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">

    <Grid>
        <ListBox Name="Files" HorizontalContentAlignment="Stretch" SelectionMode="Extended" Tag="{Binding Tag}"
                 cal:Message.Attach="[Event SelectionChanged] = [Action OnSelectionChanged($source, $eventArgs)]"
                 SelectedIndex="{Binding SelectedIndex}" d:ItemsSource="{d:SampleData ItemCount=5}">

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

                        <TextBlock Grid.Column="0" Text="{Binding Name}" FontWeight="Bold" FontFamily="Courier New" VerticalAlignment="Center" />
                        <TextBlock Grid.Column="2" Text="{Binding Size}" Margin="5,0" FontWeight="Bold" FontFamily="Courier New" VerticalAlignment="Center" Foreground="Gray"/>
                        <TextBlock Grid.Column="1" Text="{Binding DateModified}" FontWeight="Bold" FontFamily="Courier New" VerticalAlignment="Center" Foreground="Blue"/>

                    </Grid>
                </DataTemplate>
            </ListBox.ItemTemplate>
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Style.Triggers>
                        <MultiDataTrigger>
                            <MultiDataTrigger.Conditions>
                                <Condition Binding="{Binding Path=IsMouseOver, RelativeSource={RelativeSource Self}}" Value="True"/>
                                <Condition Binding="{Binding Name}" Value=""/>
                            </MultiDataTrigger.Conditions>
                            <Setter Property="Background"  Value="LightPink"/>
                        </MultiDataTrigger>
                        
                        
                    </Style.Triggers>
                </Style>
            </ListBox.ItemContainerStyle>
            <ListBox.ContextMenu>
                <ContextMenu>
                    <MenuItem Header="Delete" cal:Message.Attach="Delete($dataContext)" />
                </ContextMenu>
            </ListBox.ContextMenu>
        </ListBox>
    </Grid>
</UserControl>

ViewModel:

using Caliburn.Micro;
using FTPserver.Models;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Windows.Controls;

namespace FTPserver.ViewModels
{
    public class FilesViewModel : Screen
    {
        public string? Tag { get; set; }
        private MainViewModel? mainviewModel { get; set; }
        public List<FileDetails>? SelectedItems { get; set; }
        public BindableCollection<FileDetails>? Files {get;set;}

        private FileDetails? _selectedFile { get; set; }
        public FileDetails SelectedFile
        {
            get { return _selectedFile; }
            set
            {
                Debug.WriteLine($"file: {value.Name}");
                if (string.IsNullOrEmpty(value.Name))
                {
                    SelectedIndex = -1;
                    NotifyOfPropertyChange(() => SelectedIndex);
                    return;
                }
                _selectedFile = value;
                NotifyOfPropertyChange(() => SelectedFile);
            }
        }

        private int _selectedIndex { get; set; }
        public int SelectedIndex
        {
            get { return _selectedIndex; }
            set
            {
                Debug.WriteLine($"index: {value}");
                _selectedIndex = value;
                NotifyOfPropertyChange(() => SelectedIndex);
            }
        }
        public FilesViewModel(MainViewModel mainviewModel, string tag)
        {
            Tag = tag;
            this.mainviewModel = mainviewModel;
            Files = new BindableCollection<FileDetails>();
            SelectedIndex = -1;
        }


        public void OnSelectionChanged(ListBox sender, SelectionChangedEventArgs e)
        {
            mainviewModel!.last_fileviewModel = this;
            SelectedItems = sender.SelectedItems.Cast<FileDetails>().ToList()
                                  .Where(t => !string.IsNullOrEmpty(t.Size) &&
                                              long.TryParse(t.Size, out var n ) &&
                                              n >= 0)
                                  .ToList();
            Debug.WriteLine($"changed: {string.Join(",", SelectedItems.Select(f => f.Name))}");
        }
        public void Delete(FilesViewModel item)
        {
            var files = item.SelectedItems.Cast<FileDetails>().ToList();
        }

    }
}

My Model:

namespace FTPserver.Models
{
    public class FileDetails
    {
        public string? Name { get; set; }
        public string? Size { get; set; }
        public string? DateModified { get; set; }

        public bool IsNewer { get; set; }
        public bool IsOlder { get; set;}
        public bool? IsEqual { get; set; }
    }
}

result in image:

i am waiting when the mouse is over an item which has the Name empty to have the highlight pinky, but no, its always the default color.. i dont think caliburn is the cause of this problem....

Have i missed something?

enter image description here


Solution

  • To override the highlight color of a ListBoxItem you must override its ControlTemplate. This is because the control's native visual interaction feedback is implemented using the VisualStateManager (which is the recommended way) instead of style triggers.

    <ListBox ItemsSource="{Binding DataItems}"
              DisplayMemberPath="TextData">
      <ListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem">
          <Setter Property="Template">
            <Setter.Value>
              <ControlTemplate TargetType="ListBoxItem">
                <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                  <ContentPresenter />
                </Border>
    
                <ControlTemplate.Triggers>
                  <MultiDataTrigger>
                    <MultiDataTrigger.Conditions>
                      <Condition Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsMouseOver}"
                                 Value="True" />
                      <Condition Binding="{Binding Name}"
                                 Value="" />
                    </MultiDataTrigger.Conditions>
    
                    <Setter Property="Background"
                            Value="LightPink" />
                  </MultiDataTrigger>
                </ControlTemplate.Triggers>
              </ControlTemplate>
            </Setter.Value>
          </Setter>
        </Style>
      </ListBox.ItemContainerStyle>
    </ListBox>
    

    To modify the visual feedback only for a particular condition and otherwise keep the original visual feedback, you explicitly have to provide the original default conditions. You are creating a new instance of ControlTemplate, therefore it's a blank instance. And you can't base a ControlTemplate on a base version like you could do with a Style. As a consequence, you have to add everything from scratch.
    Of course, you could clone the original template and modify its triggers. But this will result in a very brittle implementation as you strongly depend on private implementation details of the framework (that could change any time without any notice). For this reason, I don't consider this a reasonable solution for this scenario and can't recommend it.

    You can extract the current default template using the Visual Studio XAML designer or Visual Studio Blend.

    You would retrieve the following Style and ControlTemplate which you have to merge into your overridden ControlTemplate:

    <Style x:Key="FocusVisual">
      <Setter Property="Control.Template">
        <Setter.Value>
          <ControlTemplate>
            <Rectangle Margin="2" StrokeDashArray="1 2" Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" SnapsToDevicePixels="true" StrokeThickness="1"/>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>
    
    <SolidColorBrush x:Key="Item.MouseOver.Background" Color="#1F26A0DA"/>
    <SolidColorBrush x:Key="Item.MouseOver.Border" Color="#a826A0Da"/>
    <SolidColorBrush x:Key="Item.SelectedActive.Background" Color="#3D26A0DA"/>
    <SolidColorBrush x:Key="Item.SelectedActive.Border" Color="#FF26A0DA"/>
    <SolidColorBrush x:Key="Item.SelectedInactive.Background" Color="#3DDADADA"/>
    <SolidColorBrush x:Key="Item.SelectedInactive.Border" Color="#FFDADADA"/>
    
    <Style x:Key="ListBoxItemStyle1"
           TargetType="{x:Type ListBoxItem}">
      <Setter Property="SnapsToDevicePixels"
              Value="True" />
      <Setter Property="Padding"
              Value="4,1" />
      <Setter Property="HorizontalContentAlignment"
              Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
      <Setter Property="VerticalContentAlignment"
              Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
      <Setter Property="Background"
              Value="Transparent" />
      <Setter Property="BorderBrush"
              Value="Transparent" />
      <Setter Property="BorderThickness"
              Value="1" />
      <Setter Property="FocusVisualStyle"
              Value="{StaticResource FocusVisual}" />
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="{x:Type ListBoxItem}">
            <Border x:Name="Bd"
                    Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}"
                    Padding="{TemplateBinding Padding}"
                    SnapsToDevicePixels="true">
              <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
                                VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
            </Border>
    
            <ControlTemplate.Triggers>
              <MultiTrigger>
                <MultiTrigger.Conditions>
                  <Condition Property="IsMouseOver"
                             Value="True" />
                </MultiTrigger.Conditions>
                <Setter Property="Background"
                        TargetName="Bd"
                        Value="{StaticResource Item.MouseOver.Background}" />
                <Setter Property="BorderBrush"
                        TargetName="Bd"
                        Value="{StaticResource Item.MouseOver.Border}" />
              </MultiTrigger>
              <MultiTrigger>
                <MultiTrigger.Conditions>
                  <Condition Property="Selector.IsSelectionActive"
                             Value="False" />
                  <Condition Property="IsSelected"
                             Value="True" />
                </MultiTrigger.Conditions>
                <Setter Property="Background"
                        TargetName="Bd"
                        Value="{StaticResource Item.SelectedInactive.Background}" />
                <Setter Property="BorderBrush"
                        TargetName="Bd"
                        Value="{StaticResource Item.SelectedInactive.Border}" />
              </MultiTrigger>
              <MultiTrigger>
                <MultiTrigger.Conditions>
                  <Condition Property="Selector.IsSelectionActive"
                             Value="True" />
                  <Condition Property="IsSelected"
                             Value="True" />
                </MultiTrigger.Conditions>
                <Setter Property="Background"
                        TargetName="Bd"
                        Value="{StaticResource Item.SelectedActive.Background}" />
                <Setter Property="BorderBrush"
                        TargetName="Bd"
                        Value="{StaticResource Item.SelectedActive.Border}" />
              </MultiTrigger>
              <Trigger Property="IsEnabled"
                        Value="False">
                <Setter Property="TextElement.Foreground"
                        TargetName="Bd"
                        Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
              </Trigger>
            </ControlTemplate.Triggers>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>