KeyBinding в TreeView для элементов

276
09 декабря 2017, 05:42

Пытаюсь разобраться с привязкой "горячих" клавиш к дереву. Не получается передать в качестве параметра к команде саму выбранную ветку дерева.

Хочу, чтобы при нажатии F2 открывалось окно редактирования наименования ветки дерева (или даже лучше не отдельного окна, а напрямую в самом дереве, но с этим как мне показалось еще более сложно. Хотя бы через дополнительное окно)

На текущий момент через контекстное меню работает правильно, так как к каждому элементу дерева привязано свое контекстное меню и в качестве параметра эта ветка и передается.

А вот с привязкой команды по горячей кнопке для меня оказалось сложнее.

Команда объявлена в окне:

<r:RibbonWindow.CommandBindings>
    <CommandBinding Command="{x:Static self:MainWindow.EditNodeCommand}" CanExecute="EditNodeCommandCanExecute" Executed="EditNodeCommandExecuted"></CommandBinding>
</r:RibbonWindow.CommandBindings>

Горячая клавиша объявлена в дереве:

<TreeView.InputBindings>
    <KeyBinding Command="{x:Static self:MainWindow.EditNodeCommand}" CommandParameter="{Binding}" Key="F2"></KeyBinding>
</TreeView.InputBindings>

В качестве контекста у дерева в инициализации формы присваивается список:

tp_View.DataContext = TPTree;
public ObservableCollection<TechPartTreeNode> TPTree = new ObservableCollection<TechPartTreeNode>();

Который отдельно заполняется через другие команды.

Что необходимо передать в CommandParameter, чтобы в команду e.Parameter помещался не весь список дерева, а только выбранный в данный момент? Или нужно делать вообще по другому и я не в ту сторону копаю?

PS: Команда должна привязываться именно к дереву, так как на форме будет несколько деревьев и у каждого клавиша F2 должна вызывать редактирование именно своей ветки.

Спасибо.

Answer 1

У вас команда привязана ко всему TreeView, соответственно Binding возвращает TreeView.DataContext, а он может быть каким угодно. Решение очевидное - передавать явно текущий элемент TreeView:

<TreeView Name="tv" ItemsSource="{Binding Nodes}">
    <TreeView.InputBindings>
        <KeyBinding Key="F2"
                    Command="{Binding RenameCommand}"
                    CommandParameter="{Binding ElementName=tv, Path=SelectedItem}"/>

Ну и так как при активном TreeView выбранного элемента может не быть - вы должны в коде команды первым делом проверить параметр:

void RenameNode(NodeVm node)
{
    if (node == null) return;

Либо составить правильный CanExecute, но это сложнее, думаю.

Так как же все-таки реализовать редактирование элемента прямо в дереве? Давайте попробуем сделать это!

Я воспользуюсь паттерном MVVM, у меня есть соответствующие заготовки для классов команды:

class DelegateCommand : ICommand
{
    protected readonly Predicate<object> _canExecute;
    protected readonly Action<object> _execute;
    public event EventHandler CanExecuteChanged;
    public DelegateCommand(Action<object> execute) : this(execute, _ => true) { }
    public DelegateCommand(Action<object> execute, Predicate<object> canExecute)
    {
        _execute = execute ?? throw new ArgumentNullException(nameof(execute));
        _canExecute = canExecute ?? throw new ArgumentNullException(nameof(canExecute));
    }
    public bool CanExecute(object parameter) => _canExecute(parameter);
    public void Execute(object parameter) => _execute(parameter);
    public void RaiseCanExecuteChanged()
        => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

и база для VM:

abstract class Vm : INotifyPropertyChanged
{
    protected bool Set<T>(ref T field, T value, [CallerMemberName]string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        NotifyPropertyChanged(propertyName);
        return true;
    }
    protected void NotifyPropertyChanged([CallerMemberName]string propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    public event PropertyChangedEventHandler PropertyChanged;
}

Теперь, для этого примера я написал такой класс, представляющий один элемент дерева:

class NodeVm : Vm
{
    string name;
    public string Name
    {
        get => name;
        set => Set(ref name, value, nameof(Name));
    }
    ObservableCollection<NodeVm> children;
    public ObservableCollection<NodeVm> Children
    {
        get => children;
        set => Set(ref children, value, nameof(Children));
    }
    public NodeVm() { }
    public NodeVm(string name, IEnumerable<NodeVm> children)
    {
        Name = name;
        Children = new ObservableCollection<NodeVm>(children);
    }
    public NodeVm(string name, params NodeVm[] children)
        : this(name, children.AsEnumerable()) { }
}

Теперь главная VM, она должна содержать коллекцию элементов корневого уровня, я вместо этого создал один корневой элемент, но привязку мы сделаем не к нему, а к коллекции его дочерних элементов. Также нам потребуется одно свойство для обозначения текущего редактируемого элемента и одна команда для установки этого элемента, итого:

class MainVm : Vm
{
    public NodeVm RootNode { get; }
    NodeVm editableNode;
    public NodeVm EditableNode
    {
        get => editableNode;
        set => Set(ref editableNode, value, nameof(EditableNode));
    }
    public DelegateCommand SetEditableNodeCommand { get; }
    public MainVm()
    {
        RootNode = new NodeVm("RootNode",
            new NodeVm("Node 1",
                new NodeVm("Node 7"),
                new NodeVm("Node 8",
                    new NodeVm("Node 11"),
                    new NodeVm("Node 12",
                        new NodeVm("Node 14"),
                        new NodeVm("Node 15"),
                        new NodeVm("Node 16")),
                    new NodeVm("Node 13")),
                new NodeVm("Node 9"),
                new NodeVm("Node 10")),
            new NodeVm("Node 2"),
            new NodeVm("Node 3"),
            new NodeVm("Node 4",
                new NodeVm("Node 17"),
                new NodeVm("Node 18",
                    new NodeVm("Node 21"),
                    new NodeVm("Node 22",
                        new NodeVm("Node 24"),
                        new NodeVm("Node 25"),
                        new NodeVm("Node 26")),
                    new NodeVm("Node 23")),
                new NodeVm("Node 19"),
                new NodeVm("Node 20")),
            new NodeVm("Node 5"),
            new NodeVm("Node 6"));
        SetEditableNodeCommand = new DelegateCommand(o => EditableNode = (NodeVm)o);
    }
}

Теперь разметка, я в корневой грид окна просто кладу TreeView, у меня больше ничего не будет:

<Grid Margin="5">
    <TreeView ItemsSource="{Binding RootNode.Children}">
        ...
    </TreeView>
</Grid>

Шаблон элементов TreeView будет состоять из TextBlock в обычном режиме и TextBox в режиме редактирования, поэтому просто обернем их в Grid, также я немного подшаманил со стилем TextBox, чтобы он выглядел более "плоско":

        <TreeView.ItemTemplate>
            <HierarchicalDataTemplate ItemsSource="{Binding Children}">
                <Grid>
                    <TextBlock Text="{Binding Name}">
                        ...
                    </TextBlock>
                    <TextBox Text="{Binding Name}" Margin="-2,0">
                        ...
                        <TextBox.Template>
                            <ControlTemplate TargetType="TextBox">
                                <Border x:Name="PART_ContentHost"/>
                            </ControlTemplate>
                        </TextBox.Template>
                    </TextBox>
                </Grid>
            </HierarchicalDataTemplate>
        </TreeView.ItemTemplate>

Теперь нам нужно чтобы если текущий элемент в режиме редактирования отображался TextBox, иначе TextBlock, для этого воспользуемся конвертером (я стащил его из этого ответа и немного "докрутил"):

class ObjectsEqualConverter<T> : MarkupExtension, IMultiValueConverter
{
    public T EqualValue { get; set; }
    public T NotEqualValue { get; set; }
    public object Convert(object[] values, Type tt, object p, CultureInfo ci)
        => values[0] == values[1] ? EqualValue : NotEqualValue;
    public object[] ConvertBack(object value, Type[] tt, object p, CultureInfo ci)
        => throw new NotImplementedException();
    public override object ProvideValue(IServiceProvider serviceProvider)
        => this;
}
class VisibilityEqualConverter : ObjectsEqualConverter<Visibility> { }

Добавляем в TextBlock:

                        <TextBlock.Visibility>
                            <MultiBinding Converter="{local:VisibilityEqualConverter EqualValue=Collapsed, NotEqualValue=Visible}">
                                <Binding/>
                                <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=Window}"
                                         Path="DataContext.EditableNode"/>
                            </MultiBinding>
                        </TextBlock.Visibility>

и в TextBox:

                        <TextBox.Visibility>
                            <MultiBinding Converter="{local:VisibilityEqualConverter EqualValue=Visible, NotEqualValue=Collapsed}">
                                <Binding/>
                                <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=Window}"
                                         Path="DataContext.EditableNode"/>
                            </MultiBinding>
                        </TextBox.Visibility>

Теперь, по клавише F2 нужно выбранный элемент помечать как редактируемый, это у нас уже показано как сделать:

        <TreeView.InputBindings>
            <KeyBinding Key="F2" Command="{Binding SetEditableNodeCommand}"
                        CommandParameter="{Binding SelectedItem,
                            RelativeSource={RelativeSource FindAncestor,
                                AncestorType=TreeView}}"/>
        </TreeView.InputBindings>

Ну и еще, чтобы при уходе курсора из TextBox редактируемый элемент сбрасывался нужно по событию LostFocus выполнить эту же команду с параметром null, я сделаю это с помощью пакета System.Windows.Interactivity.WPF подключенного из NuGet:

                        <i:Interaction.Triggers>
                            <i:EventTrigger EventName="LostFocus">
                                <i:InvokeCommandAction Command="{Binding DataContext.SetEditableNodeCommand,
                                                            RelativeSource={RelativeSource FindAncestor,
                                                                AncestorType=Window}}"
                                                       CommandParameter="{x:Null}"/>
                            </i:EventTrigger>
                        </i:Interaction.Triggers>

В заголовке окна подключено пространство имен: xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

Итого:

READ ALSO
Анимированные элементы в WPF C# приложении

Анимированные элементы в WPF C# приложении

С помощью каких библиотек можно реализовать что-то подобное?

243
C# экспорт базы Firebird в Excel без циклов

C# экспорт базы Firebird в Excel без циклов

Выход - использование буфера обмена или есть другой способ? Что нашел сам:

186
C# WPF PointCollection не обновляет UI

C# WPF PointCollection не обновляет UI

Необходимо сделать приложение, в котором по нажатию на кнопку к ней протягивалась бы ломанная линия, решил сделать это через Polyline

200