How to find out if selected by mouse or key?
Asked Answered
A

1

8

I have MVVM application with a WPF TreeView on the left side of a window. A details panel on the right changes content depending on the tree node selected.

If user selects a node, the content of details panel changes immediately. That's desired if user clicked on the node, but I want to delay changing content if user navigates the tree using key down/up. (Same behaviour as Windows Explorer, at least under Win XP) I assume I have to know in my ViewModel if node has been selected via mouse or keyboard.

How can I achieve this?

Update:

This is my first post hence I'm not sure if this is the right place, but I want let the community know what I did in the meantime. Here is my own solution. I'm not an expert therefore I don't know if it's a good solution. But it works for me and I would be happy if it helps others. Bugfixes, improvements or better solutions are highly appreciated.

I created below attached property HasMouseFocus...
(First I used the MouseEnterEvent but this doesn't work well if user navigates the tree with key up/down and the mouse pointer is randomly over any navigated tree item, because in that case the details gets updated immediately.)

public static bool GetHasMouseFocus(TreeViewItem treeViewItem)
{
  return (bool)treeViewItem.GetValue(HasMouseFocusProperty);
}

public static void SetHasMouseFocus(TreeViewItem treeViewItem, bool value)
{
  treeViewItem.SetValue(HasMouseFocusProperty, value);
}

public static readonly DependencyProperty HasMouseFocusProperty =
  DependencyProperty.RegisterAttached(
    "HasMouseFocus",
    typeof(bool),
    typeof(TreeViewItemProperties),
    new UIPropertyMetadata(false, OnHasMouseFocusChanged)
  );

static void OnHasMouseFocusChanged(
  DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
  TreeViewItem item = depObj as TreeViewItem;
  if (item == null)
    return;

  if (e.NewValue is bool == false)
    return;

  if ((bool)e.NewValue)
  {
    item.MouseDown += OnMouseDown;
    item.MouseLeave += OnMouseLeave;
  }
  else
  {
    item.MouseDown -= OnMouseDown;
    item.MouseLeave -= OnMouseLeave;
  }
}

/// <summary>
/// Set HasMouseFocusProperty on model of associated element.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
static void OnMouseDown(object sender, MouseEventArgs e)
{
  if (sender != e.OriginalSource)
    return;

  TreeViewItem item = sender as TreeViewItem;
  if ((item != null) & (item.HasHeader))
  {
    // get the underlying model of current tree item
    TreeItemViewModel header = item.Header as TreeItemViewModel;
    if (header != null)
    {
      header.HasMouseFocus = true;
    }
  }
}

/// <summary>
/// Clear HasMouseFocusProperty on model of associated element.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
static void OnMouseLeave(object sender, MouseEventArgs e)
{
  if (sender != e.OriginalSource)
    return;

  TreeViewItem item = sender as TreeViewItem;
  if ((item != null) & (item.HasHeader))
  {
    // get the underlying model of current tree item
    TreeItemViewModel header = item.Header as TreeItemViewModel;
    if (header != null)
    {
      header.HasMouseFocus = false;
    }
  }
}

...and applied it to the TreeView.ItemContainerStyle

    <TreeView.ItemContainerStyle>
      <Style TargetType="{x:Type TreeViewItem}" >
        <!-- These Setters binds some properties of a TreeViewItem to the TreeViewItemViewModel. -->
        <Setter Property="IsExpanded" Value="{Binding Path=IsExpanded, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        <Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        <Setter Property="ToolTip" Value="{Binding Path=CognosBaseClass.ToolTip}"/>
        <!-- These Setters applies attached behaviors to all TreeViewItems. -->
        <Setter Property="properties:TreeViewItemProperties.PreviewMouseRightButtonDown" Value="True" />
        <Setter Property="properties:TreeViewItemProperties.BringIntoViewWhenSelected" Value="True" />
        <Setter Property="properties:TreeViewItemProperties.HasMouseFocus" Value="True" />
      </Style>
    </TreeView.ItemContainerStyle>

Where properties is the path of my attached property.

    xmlns:properties="clr-namespace:WPF.MVVM.AttachedProperties;assembly=WPF.MVVM"

Then in my ViewModel, if HasMousefocusProperty is true, I update the details panel (GridView) immediately. If false I simply start a DispatcherTimer and apply the currently selected item as Tag. After an Interval of 500ms the Tick-Event applies the details, but only if the selected item is still the same as Tag.

/// <summary>
/// This property is beeing set when the selected item of the tree has changed.
/// </summary>
public TreeItemViewModel SelectedTreeItem
{
  get { return Property(() => SelectedTreeItem); }
  set
  {
    Property(() => SelectedTreeItem, value);
    if (this.SelectedTreeItem.HasMouseFocus)
    {
      // show details for selected node immediately
      ShowGridItems(value);
    }
    else
    {
      // delay showing details
      this._selctedNodeChangedTimer.Stop();
      this._selctedNodeChangedTimer.Tag = value;
      this._selctedNodeChangedTimer.Start();
    }
  }
}
Amelita answered 17/6, 2012 at 8:48 Comment(3)
The top two answers in the following SO question deal with the keybinding issue: https://mcmap.net/q/326900/-keyboard-events-in-a-wpf-mvvm-application and https://mcmap.net/q/326900/-keyboard-events-in-a-wpf-mvvm-application As far as your personal situation, either play around or wait for someone to give you a fish.Rianon
Many thanks @Geoist. Using a cammand binding I could set a flag in my ViewModel as soon as the user presses the specified key(s). But I have to clear this flag again when (or before) then user selects another tree node. Any ideas?Amelita
@Amelita Yes, you are right you will have to take care of resetting this flag, the best place woould be the logic where you are initiating the refresh of your details panel.Ferwerda
F
2

You can handle the OnPreviewKeyDown for your TreeView(or user control having it) and programmatically set a flag in your ViewModel and consider it while refreshing details panel -

protected override void OnPreviewKeyDown(System.Windows.Input.KeyEventArgs e)
{
    switch(e.Key)
    {
        case Key.Up:
        case Key.Down:
           MyViewModel.IsUserNavigating = true;
           break;
    }
}

A similar approch and other solutions are mentioned in this SO question -

How can I programmatically navigate (not select, but navigate) a WPF TreeView?

Update: [In response to AalanY's comment]

I don't think there is any problem in having some code-behind in Views, that doesn't break MVVM.

In the article, WPF Apps With The Model-View-ViewModel Design Pattern, the author who is Josh Smith says:

In a well-designed MVVM architecture, the codebehind for most Views should be empty, or, at most, only contain code that manipulates the controls and resources contained within that view. Sometimes it is also necessary to write code in a View's codebehind that interacts with a ViewModel object, such as hooking an event or calling a method that would otherwise be very difficult to invoke from the ViewModel itself.

In my experience it's impossible to build an enterprise(of considerable size) application without having any code-behind, specially when you have to use complex controls like TreeView, DataGrid or 3'rd party controls.

Ferwerda answered 17/6, 2012 at 12:1 Comment(4)
Thank you for your answer @akjoshi, but I'm using the MVVM pattern and I don't want to have any code behind.Amelita
Thanks again @akjoshi, I have already read this great article of Josh Smith and of course you are right. What do you think about my solution? Please see my update above...Amelita
@Amelita Your solution looks good as it can be attached to a treeview in XAML and is having elements of reusability; except this it's almost same as using event handlers and updating ViewModel flag.Ferwerda
@Amelita If you can make this behavior more generic by passing/refering a boolean property in this behavior instead of 'item.Header as TreeItemViewModel' then this will be a very good solution. Not sure how to do that though, may be by creating another attached property of bindingExpression type or something like that....Ferwerda

© 2022 - 2024 — McMap. All rights reserved.