1

問題:

以下のサンプルでは、​​左側の列に 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 つの悪い副作用がありました。

  1. MessageBox が表示されると、TreeViewItem は強制的に編集モードを終了します。
  2. 編集モードで 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
}
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 に答える