Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to find out if selected by mouse or key?

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();
    }
  }
}
like image 850
AelanY Avatar asked Jun 17 '12 08:06

AelanY


1 Answers

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.

like image 70
akjoshi Avatar answered Sep 26 '22 09:09

akjoshi