1

I created a simple, stripped ListView style that highlights an element when the IsMouseOver property is true. This is done by triggering in the ItemContainerStyle. This works great and the xaml is like this:

<ListView>

  <ListView.ItemTemplate>
    <DataTemplate>
      <!--UserControl with actual content goes here-->
     </DataTemplate>
   </ListView.ItemTemplate>

   <ListView.ItemContainerStyle>
     <Style TargetType="{x:Type ListViewItem}">

       <Setter Property="Template">
         <Setter.Value>
           <ControlTemplate TargetType="ListViewItem">
             <!--here is a  border with the ContentPresenter inside-->
           </ControlTemplate>
         </Setter.Value>
       </Setter>

       <Style.Triggers>
         <Trigger Property="IsMouseOver" Value="True">
           <Setter Property="Background" Value="Lime"/>                                         
         </Trigger>
      </Style.Triggers>

    </Style>
  <ListView.ItemContainerStyle>
</ListView>

However I would also like that the color set on hovering stays when the actual element's contextmenu is shown by right-clicking it. Basically the question is like this one, except that I cannot use the (otherwise great) answer there: the idea is to add a trigger to check when the contextmenu is open:

<DataTrigger Binding="{Binding ContextMenu.IsOpen}" Value="True">
  <Setter Property="Background" Value="Lime"/>
</DataTrigger>

The question is: what binding expression do I enter in order to figure out that ContextMenu.IsOpen on the actual content set in the DataTemplate? I tried all sort of things like referring to ContentPresenter.ContextMenu.IsOpen etc but none worked.

Apart from using ContextMenu.IsOpen, I already tried tons of combinations of triggers on IsSelected, event triggers on MouseLeave etc but also to no avail. So the second question is: if the contextmenu trick does not work, is there another way to get this effect? Basically I want a list view that does not support selecting of any kind, but does show the user at which element the mouse is, no matter is a menu is partly hiding it or not.

Community
  • 1
  • 1
stijn
  • 34,664
  • 13
  • 111
  • 163

1 Answers1

-1

As uausal when something seems hard in xaml, this was perfectly solvable using attached properties with the additional bonus of being reuasable. The basic principle is to attach a behavior to a FrameworkElement, and hook it's MouseEneter/Leave events. Apart from those, also look for any children having a contextmenu and hook the ContextMenuOpening/Closing events. I don't have a blog or repository so here is the code, I imagine this can be useful for others as well.

public static class HasMouseOver
{
  private static readonly DependencyProperty HasMouseOverBehaviorProperty =        DependencyProperty.RegisterAttached(
          "HasMouseOverBehavior", typeof( HasMouseOverBehavior ),
          typeof( FrameworkElement ), null );

  private static void AttachBehavior( FrameworkElement target )
  {
    var behavior = target.GetValue( HasMouseOverBehaviorProperty ) as HasMouseOverBehavior;
    if( behavior == null )
    {
      behavior = new HasMouseOverBehavior( target, HasMouseProperty );
      target.SetValue( HasMouseOverBehaviorProperty, behavior );
    }
  }

  private static void DetachBehavior( FrameworkElement target )
  {
    target.ClearValue( HasMouseOverBehaviorProperty );
  }

  public static readonly DependencyProperty RegisterProperty = DependencyProperty.RegisterAttached(
          "Register", typeof( bool ),
          typeof( HasMouseOver ), new PropertyMetadata( false, RegisterPropertyChanged ) );

  public static void SetRegister( FrameworkElement element, bool value )
  {
    element.SetValue( RegisterProperty, value );
  }    
  public static bool GetRegister( FrameworkElement element )
  {
    return (bool) element.GetValue( RegisterProperty );
  }

  private static void RegisterPropertyChanged( DependencyObject d, DependencyPropertyChangedEventArgs e )
  {
    var target = d as FrameworkElement;
    if( target == null )
      return;
    if( (bool) e.NewValue )
      AttachBehavior( target );
    else
      DetachBehavior( target );
  }

  public static readonly DependencyProperty HasMouseProperty = DependencyProperty.RegisterAttached(
      "HasMouse", typeof( bool ),
      typeof( HasMouseOver ), null );

  public static void SetHasMouse( FrameworkElement element, bool value )
  {
    element.SetValue( HasMouseProperty, value );
  }    
  public static bool GetHasMouse( FrameworkElement element )
  {
    return (bool) element.GetValue( HasMouseProperty );
  }
}

public class HasMouseOverBehavior
{
  private readonly DependencyProperty dep;
  private readonly FrameworkElement target;
  private bool isReallyLeaving;

  public HasMouseOverBehavior( FrameworkElement target, DependencyProperty dep )
  {
    this.target = target;
    this.dep = dep;
    target.Loaded += Loaded;
    target.Unloaded += Unloaded;
    target.MouseEnter += MouseEnter;
    target.MouseLeave += MouseLeave;
  }

  private void Loaded( object sender, RoutedEventArgs e )
  {
    var childrenWithMenu = target.FindChildren<FrameworkElement>( u => u.ContextMenu != null );
    foreach( var child in childrenWithMenu )
    {
      child.ContextMenuOpening += ContextMenuOpening;
      child.ContextMenuClosing += ContextMenuClosing;
    }
  }

  private void Unloaded( object sender, RoutedEventArgs e )
  {
    var childrenWithMenu = target.FindChildren<FrameworkElement>( u => u.ContextMenu != null );
    foreach( var child in childrenWithMenu )
    {
      child.ContextMenuOpening -= ContextMenuOpening;
      child.ContextMenuClosing -= ContextMenuClosing;
    }
  }

  private void ContextMenuOpening( object sender, ContextMenuEventArgs e )
  {
    isReallyLeaving = false;
  }

  private void ContextMenuClosing( object sender, ContextMenuEventArgs e )
  {
    if( !isReallyLeaving )  //else, mouse is still over element eg upon Esc.
      DoesNotHaveMouse();
  }

  private void MouseEnter( object sender, System.Windows.Input.MouseEventArgs e )
  {
    isReallyLeaving = true;
    HasMouse();
  }

  private void MouseLeave( object sender, System.Windows.Input.MouseEventArgs e )
  {
    if( isReallyLeaving )
    {
      isReallyLeaving = false;
      DoesNotHaveMouse();
    }
  }

  private void HasMouse()
  {
    target.SetValue( dep, true );
  }

  private void DoesNotHaveMouse()
  {
    target.SetValue( dep, false );
  }
}

And in xaml:

<style>
  <Setter Property="behav:HasMouseOver.Register" Value="True"/>
  <Style.Triggers>
    <Trigger Property="behav:HasMouseOver.HasMouse" Value="True">
      ...
    </Trigger>
  </Style.Triggers>
</style>
stijn
  • 34,664
  • 13
  • 111
  • 163
  • Not working. Getting error on `target.FindChildren`: `'FrameworkElement' does not contain a definition for 'FindChildren' and no accessible extension method 'FindChildren' accepting a first argument of type 'FrameworkElement' could be found (are you missing a using directive or an assembly reference?)` – Sam White Jun 15 '22 at 15:05
  • @SamWhite fair point, you need an extension method for getting children like the answers found here: https://stackoverflow.com/questions/10279092/how-to-get-children-of-a-wpf-container-by-type – stijn Jun 16 '22 at 05:54
  • Thanks, that works. Though there's one issue with your method: Right-click on one item then immediately right-click on a second item, and then on a third, and so on, the mouse-over effect remains on all previous items. Until you mouse over it when the context menu isn't open. – Sam White Jun 16 '22 at 21:31
  • @SamWhite must be an incorrect interaction revolving around that `isLeaving` flag. From the loos of it, tracking amount of ContextMenuOpening/Leaving calls with a counter could solve it, not sure – stijn Jun 18 '22 at 20:24