1

问题:

在下面的示例中,我在左列中有一个 TreeView,在右列中有一个 ListBox。TreeView 显示一小部分示例项目。当用户选择 TreeViewItem 并按 F2 时,该项目通过将其 TextBlock 替换为 TextBox 进入“编辑模式”。

现在,如果我选择第一个 TreeViewItem 并将其置于编辑模式,然后左键单击第二个 TreeViewItem,则第一个项目离开编辑模式,正如预期的那样。

但是,如果我将第一个 TreeViewItem 置于编辑模式,然后在 ListBox 内单击,则 TreeViewItem 仍处于编辑模式。

当用户在 TreeView 之外单击时,导致 TreeViewItem 离开编辑模式的可靠方法是什么?当然,请不要建议我只是在 ListBox 中添加一个鼠标侦听器;我正在寻找一个强大的解决方案。


我最好的解决方法:

我尝试将 IsKeyboardFocusWithinChanged 事件侦听器添加到 TreeView:

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

虽然这确实解决了我的问题,但它有两个不好的副作用:

  1. 当 MessageBox 出现时,TreeViewItem 被迫离开编辑模式。
  2. 如果我在编辑模式下右键单击 TreeViewItem,则会导致 TreeViewItem 离开编辑模式。这使我无法在 TreeViewItem 的文本框中使用上下文菜单。

示例代码:

(此示例可以从 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>

主窗口.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; }
    }
}

文件.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
}
4

2 回答 2

1

当您失去对 TreeViewItem 的关注时,您可以添加一个事件处理程序。

视图模型(或数据上下文)中的事件处理程序方法:

 /// <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!");
 }

TreeViewItem LostFocus 的 XAML:

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

用于 TreeView LostFocus 的 Xaml:

 <TreeView Name="myTreeView">
      <TreeView.Style>
           <Style TargetType="{x:Type TreeView}">
                <EventSetter Event="TreeView.LostFocus" Handler="treeView_FocusLoser" />
           </Style>
      </TreeView.Style>
 </TreeView>
于 2012-12-04T20:17:19.237 回答
0

我遇到了同样的问题,但即使用户在框外点击不可聚焦的元素,也需要触发焦点丢失。我找到了一个解决方案,但它并不漂亮:

在 UI 的主容器元素上,为PreviewMouseDown事件创建一个事件处理程序。然后在事件处理程序中,找出点击的来源以及是否需要处理:

    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;
    }
于 2012-12-04T20:25:54.537 回答