問題:
以下のサンプルでは、左側の列に TreeView があり、右側の列に ListBox があります。TreeView には、サンプル アイテムの小さなリストが表示されます。ユーザーが TreeViewItem を選択して F2 キーを押すと、アイテムは TextBlock を TextBox に置き換えることによって「編集モード」になります。
ここで、最初の TreeViewItem を選択して編集モードにし、2 番目の TreeViewItem を左クリックすると、予想どおり、最初の項目が編集モードから離れます。
ただし、最初の TreeViewItem を編集モードにしてから ListBox 内をクリックすると、TreeViewItem は編集モードのままになります。
ユーザーが TreeView の外側をクリックしたときに TreeViewItem が編集モードを終了する確実な方法は何ですか? もちろん、マウス リスナを ListBox に追加するだけではありません。堅牢なソリューションを探しています。
解決するための私の最善の試み:
TreeView に IsKeyboardFocusWithinChanged イベント リスナーを追加してみました。
private static void IsKeyboardFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var treeView = sender as TreeView;
if (treeView != null && !treeView.IsKeyboardFocusWithin)
{
EditEnding(treeView, false);
}
}
これで問題は解決しましたが、2 つの悪い副作用がありました。
- MessageBox が表示されると、TreeViewItem は強制的に編集モードを終了します。
- 編集モードで TreeViewItem 内を右クリックすると、TreeViewItem が編集モードを終了します。これにより、TreeViewItem の TextBox でコンテキスト メニューを使用できなくなります。
サンプルコード:
(このサンプルは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
}