1
votes

Problem:

In the sample below, I have a TreeView in a left column and a ListBox in a right column. The TreeView displays a small list of sample items. When a user selects a TreeViewItem and presses F2, the item goes into 'edit mode' by replacing its TextBlock with a TextBox.

Now, if I select the first TreeViewItem and put it in edit mode, and then left click on the second TreeViewItem, the first item leaves edit mode, as would be expected.

However, if I put the first TreeViewItem in edit mode and then click inside the ListBox, the TreeViewItem remains in edit mode.

What's a robust way of causing the TreeViewItem to leave edit mode when a user clicks outside of its TreeView? Naturally, please don't propose that I simply add a mouse listener to the ListBox; I'm looking for a robust solution.


My best attempt to solve:

I tried adding an IsKeyboardFocusWithinChanged event listener to the TreeView:

private static void IsKeyboardFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs e)
{
    var treeView = sender as TreeView;
    if (treeView != null && !treeView.IsKeyboardFocusWithin)
    {
        EditEnding(treeView, false);
    }
}

While this did solve my problem, it had two bad side effects:

  1. When a MessageBox appears, the TreeViewItem is forced to leave edit mode.
  2. If I right click inside a TreeViewItem in edit mode, it causes the TreeViewItem to leave edit mode. This prevents me from using context menus in my TreeViewItem's TextBox.

Sample code:

(This sample can be downloaded from Skydrive)

MainWindow.xaml:

<Window 
x:Class="WpfApplication3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
xmlns:wpfApplication3="clr-namespace:WpfApplication3"
Title="MainWindow" Height="350" Width="525"
>
<Window.Resources>
    <DataTemplate x:Key="viewNameTemplate">
        <TextBlock 
            Text="{Binding Name}"
            FontStyle="Normal"
            VerticalAlignment="Center"
            />
    </DataTemplate>

    <DataTemplate x:Key="editNameTemplate">
        <TextBox
            Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"
            VerticalAlignment="Center"
            />
    </DataTemplate>

    <Style x:Key="editableContentControl"
        TargetType="{x:Type ContentControl}"
        >
        <Setter
            Property="ContentTemplate"
            Value="{StaticResource viewNameTemplate}"
            />
        <Setter
            Property="Focusable"
            Value="False"
            />
        <Style.Triggers>
            <DataTrigger
                Binding="{Binding Path=IsInEditMode}"
                Value="True"
                >
                <Setter
                    Property="ContentTemplate"
                    Value="{StaticResource editNameTemplate}"
                    />
            </DataTrigger>
        </Style.Triggers>
    </Style>
</Window.Resources>
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <TreeView
        Grid.Column="0"
        wpfApplication3:EditSelectedItemBehavior.IsEnabled="{Binding RelativeSource={RelativeSource Self}, Path=IsVisible}"
        ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type wpfApplication3:MainWindow}}, Path=Files}"
        >
        <TreeView.ItemTemplate>
            <DataTemplate>
                <ContentControl 
                    Content="{Binding}" 
                    Focusable="False"
                    Style="{StaticResource editableContentControl}" 
                    />
            </DataTemplate>
        </TreeView.ItemTemplate>
    </TreeView>
    <ListBox
        Grid.Column="1"
        ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type wpfApplication3:MainWindow}}, Path=Files}"
        />
</Grid>
</Window>

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        Files = new ObservableCollection<File>();
        Files.Add(new File("A.txt"));
        Files.Add(new File("B.txt"));
        Files.Add(new File("C.txt"));
        Files.Add(new File("D.txt"));

        InitializeComponent();
    }

    public ObservableCollection<File> Files { get; private set; }
}

EditSelectedItemBehavior.cs

public static class EditSelectedItemBehavior
{
    public static bool GetIsEnabled(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsEnabledProperty);
    }

    public static void SetIsEnabled(DependencyObject obj, bool value)
    {
        obj.SetValue(IsEnabledProperty, value);
    }

    public static readonly DependencyProperty IsEnabledProperty =
        DependencyProperty.RegisterAttached(
            "IsEnabled",
            typeof(bool),
            typeof(EditSelectedItemBehavior),
            new UIPropertyMetadata(false, OnIsEnabledChanged));

    private static void OnIsEnabledChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        var treeView = obj as TreeView;
        if (treeView == null)
        {
            return;
        }

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

        if ((bool)e.NewValue)
        {
            treeView.CommandBindings.Add(new CommandBinding(TransactionCommands.Cancel, CancelExecuted));
            treeView.CommandBindings.Add(new CommandBinding(TransactionCommands.Commit, CommitExecuted));
            treeView.CommandBindings.Add(new CommandBinding(TransactionCommands.Edit, EditExecuted));

            treeView.InputBindings.Add(new KeyBinding(TransactionCommands.Cancel, Key.Escape, ModifierKeys.None));
            treeView.InputBindings.Add(new KeyBinding(TransactionCommands.Commit, Key.Enter, ModifierKeys.None));
            treeView.InputBindings.Add(new KeyBinding(TransactionCommands.Edit, Key.F2, ModifierKeys.None));

            treeView.SelectedItemChanged += SelectedItemChanged;
            treeView.Unloaded += Unloaded;
        }
        else
        {
            for (var i = treeView.CommandBindings.Count - 1; i >= 0; i--)
            {
                var commandBinding = treeView.CommandBindings[i];
                if (commandBinding != null && (commandBinding.Command == TransactionCommands.Cancel || commandBinding.Command == TransactionCommands.Commit || commandBinding.Command == TransactionCommands.Edit))
                {
                    treeView.CommandBindings.RemoveAt(i);
                }
            }

            for (var i = treeView.InputBindings.Count - 1; i >= 0; i--)
            {
                var keyBinding = treeView.InputBindings[i] as KeyBinding;
                if (keyBinding != null && (keyBinding.Command == TransactionCommands.Cancel || keyBinding.Command == TransactionCommands.Commit || keyBinding.Command == TransactionCommands.Edit))
                {
                    treeView.InputBindings.RemoveAt(i);
                }
            }

            treeView.SelectedItemChanged -= SelectedItemChanged;
            treeView.Unloaded -= Unloaded;
        }
    }

    private static void SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        var treeView = sender as TreeView;
        if (treeView != null)
        {
            EditEnding(treeView, true);
        }
    }

    private static void Unloaded(object sender, RoutedEventArgs e)
    {
        var treeView = sender as TreeView;
        if (treeView != null)
        {
            EditEnding(treeView, false);
        }
    }

    private static void EditExecuted(object sender, ExecutedRoutedEventArgs e)
    {
        var treeView = sender as TreeView;
        if (treeView != null)
        {
            EditExecuted(treeView);
        }
    }

    private static void CommitExecuted(object sender, ExecutedRoutedEventArgs e)
    {
        var treeView = sender as TreeView;
        if (treeView != null)
        {
            EditEnding(treeView, true);
        }
    }

    private static void CancelExecuted(object sender, ExecutedRoutedEventArgs e)
    {
        var treeView = sender as TreeView;
        if (treeView != null)
        {
            EditEnding(treeView, false);
        }
    }

    private static void EditExecuted(TreeView treeView)
    {
        if (!TreeViewAttachedProperties.GetIsEditingObject(treeView))
        {
            var editableObject = treeView.SelectedItem as IEditableObject;
            TreeViewAttachedProperties.SetEditableObject(treeView, editableObject);

            if (editableObject != null)
            {
                TreeViewAttachedProperties.SetIsEditingObject(treeView, true);
                editableObject.BeginEdit();
            }
        }
    }

    private static void EditEnding(TreeView treeView, bool commitEdit)
    {
        if (TreeViewAttachedProperties.GetIsEditingObject(treeView))
        {
            TreeViewAttachedProperties.SetIsEditingObject(treeView, false);

            var editableObject = TreeViewAttachedProperties.GetEditableObject(treeView);
            if (editableObject != null)
            {
                if (commitEdit)
                {
                    try
                    {
                        editableObject.EndEdit();
                    }
                    catch (ArgumentOutOfRangeException aex)
                    {
                        // This is a hackaround for renaming a Biml file in Mist's project tree view,
                        // where committing an edit triggers an OutOfRange exception, despite the edit working properly.
                        Console.WriteLine(aex.Message + " " + aex.InnerException);
                    }
                }
                else
                {
                    editableObject.CancelEdit();
                }
            }
        }
    }
}  

TreeViewAttachedProperties.cs

public static class TreeViewAttachedProperties
{
    public static readonly DependencyProperty EditableObjectProperty =
           DependencyProperty.RegisterAttached(
               "EditableObject",
               typeof(IEditableObject),
               typeof(TreeViewAttachedProperties));

    [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Derived type is intentionally used to restrict the parameter type.")]
    public static void SetEditableObject(TreeView treeView, IEditableObject obj)
    {
        treeView.SetValue(EditableObjectProperty, obj);
    }

    [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Derived type is intentionally used to restrict the parameter type.")]
    public static IEditableObject GetEditableObject(TreeView treeView)
    {
        return (IEditableObject)treeView.GetValue(EditableObjectProperty);
    }

    public static readonly DependencyProperty IsEditingObjectProperty =
        DependencyProperty.RegisterAttached(
           "IsEditingObject",
           typeof(bool),
           typeof(TreeViewAttachedProperties));

    [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Derived type is intentionally used to restrict the parameter type.")]
    public static void SetIsEditingObject(TreeView treeView, bool value)
    {
        treeView.SetValue(IsEditingObjectProperty, value);
    }

    [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Derived type is intentionally used to restrict the parameter type.")]
    public static bool GetIsEditingObject(TreeView treeView)
    {
        return (bool)treeView.GetValue(IsEditingObjectProperty);
    }
}

TransactionCommands.cs:

public static class TransactionCommands
{
    private static readonly RoutedUICommand _edit = new RoutedUICommand("Edit", "Edit", typeof(TransactionCommands));

    public static RoutedUICommand Edit
    {
        get { return _edit; }
    }

    private static readonly RoutedUICommand _cancel = new RoutedUICommand("Cancel", "Cancel", typeof(TransactionCommands));

    public static RoutedUICommand Cancel
    {
        get { return _cancel; }
    }

    private static readonly RoutedUICommand _commit = new RoutedUICommand("Commit", "Commit", typeof(TransactionCommands));

    public static RoutedUICommand Commit
    {
        get { return _commit; }
    }

    private static readonly RoutedUICommand _delete = new RoutedUICommand("Delete", "Delete", typeof(TransactionCommands));

    public static RoutedUICommand Delete
    {
        get { return _delete; }
    }

    private static readonly RoutedUICommand _collapse = new RoutedUICommand("Collapse", "Collapse", typeof(TransactionCommands));

    public static RoutedUICommand Collapse
    {
        get { return _collapse; }
    }
}

File.cs:

public class File : IEditableObject, INotifyPropertyChanged
{
    private bool _editing;
    private string _name;

    public File(string name)
    {
        _name = name;
    }

    public string Name
    {
        get
        {
            return _name;
        }

        set
        {
            if (_name != value)
            {
                _name = value;
                OnPropertyChanged("Name");
            }
        }
    }

    #region IEditableObject

    [Browsable(false)]
    protected string CachedName
    {
        get;
        private set;
    }

    [Browsable(false)]
    public bool IsInEditMode
    {
        get { return _editing; }
        private set
        {
            if (_editing != value)
            {
                _editing = value;
                OnPropertyChanged("IsInEditMode");
            }
        }
    }

    public virtual void BeginEdit()
    {
        // Save name before entering edit mode.
        CachedName = Name;
        IsInEditMode = true;
    }

    [EnvironmentPermission(SecurityAction.Demand, Unrestricted = true)]
    public virtual void EndEdit()
    {
        CachedName = string.Empty;
        IsInEditMode = false;
    }

    public void CancelEdit()
    {
        if (IsInEditMode)
        {
            if (CachedName != null)
            {
                Name = CachedName;
            }

            CachedName = string.Empty;
            IsInEditMode = false;
        }
    }

    public void SetCachedName(string cachedName)
    {
        CachedName = cachedName;
    }

    #endregion

    #region INotifyPropertyChanged

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string property)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
    }

    #endregion
}
2

2 Answers

1
votes

You can add an event handler for when you lose focus on the TreeViewItem.

Event handler method in viewmodel (or data context):

 /// <summary>
 /// This is a template method to show that something occurs when you lose focus on the TreeViewItem
 /// </summary>
 /// <param name="sender">TreeViewItem</param>
 /// <param name="e">Routed Event arguments</param>
 public void treeView_FocusLoser(object sender, RoutedEventArgs e) {
      MessageBox.Show("Argg!");
 }

XAML for TreeViewItem LostFocus:

 <TreeView Name="myTreeView">
      <TreeView.ItemContainerStyle>
           <Style TargetType="{x:Type TreeViewItem}">
                <EventSetter Event="TreeViewItem.LostFocus" Handler="treeView_FocusLoser" />
           </Style>
      </TreeView.ItemContainerStyle>
 </TreeView>

Xaml for TreeView LostFocus:

 <TreeView Name="myTreeView">
      <TreeView.Style>
           <Style TargetType="{x:Type TreeView}">
                <EventSetter Event="TreeView.LostFocus" Handler="treeView_FocusLoser" />
           </Style>
      </TreeView.Style>
 </TreeView>
0
votes

I ran into the same issue, but needed to also trigger the focus lost even when the user clicks outside of the box, on a non-focusable element. I found a solution for this, but it wasn't pretty:

On the main container element of your UI, create an eventhandler for the PreviewMouseDown event. Then in the eventhandler, figure out where the click comes from and if it needs to be handled:

    private void GridPreviewMouseDown(object sender, MouseButtonEventArgs e)
    {
        var parent = FindVisualParent<StackPanel>((DependencyObject)e.OriginalSource);
        if (parent != null && parent.Tag == "IgnoreClickPanel")
        {
            //ignore previewclicks from these controls
        }
        else
        {
            //prism eventaggregator will notify all user controls which care about this
            eventAggregator.GetEvent<MouseDownEvent>().Publish(true);
        }
        e.Handled = false;
    }