问题:
在下面的示例中,我在左列中有一个 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);
}
}
虽然这确实解决了我的问题,但它有两个不好的副作用:
- 当 MessageBox 出现时,TreeViewItem 被迫离开编辑模式。
- 如果我在编辑模式下右键单击 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
}