Hooking-up commands in AvalonEdit used in ListView's ItemTemplate doesn't work

.net, avalonedit, binding, c#, wpf

Solution

This is what I used to get past similar issues - I hope it's useful to people (this general logic can be applied to a wide array of Avalon editor related issues)...

What actually happens is probably the Avalon's fault (in combination with `ListItem` etc.). It messes up the mouse handling and I'm guessing the focus (which should be on the `TextArea` for commands and `CanExecute` to work.

The `mouse handling` is the issue - as if you just press `windows context menu` key it pops up a regular menu with enabled commands. Avalon editor has a complex mouse/key handling (it's hard to make a good editor) - and on keyboard it does an explicit '`focus`' on the TextArea. You can also see the issue by putting a breakpoint on the `CanCutOrCopy` method (`Editing/EditingCommandHandler.cs`, download the Avalon source) which actually handles the `ApplicationCommands.Copy`. For the 'keyboard' menu it first goes in there, then pops up. For the 'mouse' one, it pops up - and then on exit it checks the `CanExecute` (enters that method). That's all wrong!

And the errata...

There are no problems with your own commands, just expose your commands normally and all should work.

For `ApplicationCommands` (i.e. the `RoutedCommand`) it doesn't wire up properly - and the `Execute`, `CanExecute` don't go where it should, i.e. the `TextArea`. To correct that you need to `rewire` the commands into your own wrappers - and basically call the TextArea handling - which is just a few lines of code, but it's a necessary step (I don't think there is a more 'beautiful' solution to this, short of fixing the Avalon code - which may be a pain, never crossed my mind).

(all is based on your example - fill in the blanks where I left out) Your XAML:

<Window.Resources>
    <DataTemplate DataType="{x:Type my:myClass}">
        <StackPanel>
            <my:AvalonTextEditor x:Name="xmlMessage" SyntaxHighlighting="XML" ShowLineNumbers="True" EditText="{Binding text}" >
                <my:AvalonTextEditor.ContextMenu>
                    <ContextMenu x:Name="mymenu1">
                        <ContextMenu.Resources>
                            <Style TargetType="MenuItem">
                                <Setter Property="CommandParameter" Value="{Binding Path=., RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
                            </Style>
                        </ContextMenu.Resources>
                        <MenuItem Header="My Copy" Command="{Binding CopyCommand}" />
                        <MenuItem Header="My Paste" Command="{Binding PasteCommand}" />
                        <MenuItem Header="My Cut" Command="{Binding CutCommand}" />
                        <MenuItem Header="My Undo" Command="{Binding UndoCommand}" />
                        <MenuItem Header="My Redo" Command="{Binding RedoCommand}" />
                        <Separator />
                        <MenuItem Command="Undo" />
                        <MenuItem Command="Redo" />
                        <Separator/>
                        <MenuItem Command="Cut" />
                        <MenuItem Command="Copy" />
                        <MenuItem Command="Paste" />
                    </ContextMenu>
                </my:AvalonTextEditor.ContextMenu>
            </my:AvalonTextEditor>
        </StackPanel>
    </DataTemplate>
</Window.Resources>
<StackPanel>
    <DockPanel>
        <ListView ItemsSource="{Binding collection}" />
        <ContentControl Content="{Binding mc}" />
    </DockPanel>
</StackPanel>

The code behind - view model: (note: I left the naming as you did put it - but please do not use small-caps for props :)

public class MyViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public MyViewModel()
    {
        collection = new ObservableCollection<myClass>(new[]
        {
            new myClass{ text = "some more test - some more test - some more test - some more test - some more test - some more test - some more test - some more test - some more test - " },
            new myClass{ text = "test me test me = test me test me = test me test me = test me test me = test me test me = test me test me = " },
            new myClass{ text = "test again - test again - test again - test again - test again - " },
            new myClass{ text = "test again - test again - " },
            new myClass{ text = "test again - " },
            new myClass{ text = "test" },
        });
        mc = new myClass();
    }
    public ObservableCollection<myClass> collection { get; set; }
    public myClass mc { get; set; }
}

public class myClass
{
    public string text { get; set; }

    AvalonRelayCommand _copyCommand;
    public AvalonRelayCommand CopyCommand
    { get { return _copyCommand ?? (_copyCommand = new AvalonRelayCommand(ApplicationCommands.Copy) { Text = "My Copy" }); } }

    AvalonRelayCommand _pasteCommand;
    public AvalonRelayCommand PasteCommand
    { get { return _pasteCommand ?? (_pasteCommand = new AvalonRelayCommand(ApplicationCommands.Paste) { Text = "My Paste" }); } }

    AvalonRelayCommand _cutCommand;
    public AvalonRelayCommand CutCommand
    { get { return _cutCommand ?? (_cutCommand = new AvalonRelayCommand(ApplicationCommands.Cut) { Text = "My Cut" }); } }

    AvalonRelayCommand _undoCommand;
    public AvalonRelayCommand UndoCommand
    { get { return _undoCommand ?? (_undoCommand = new AvalonRelayCommand(ApplicationCommands.Undo) { Text = "My Undo" }); } }

    AvalonRelayCommand _redoCommand;
    public AvalonRelayCommand RedoCommand
    { get { return _redoCommand ?? (_redoCommand = new AvalonRelayCommand(ApplicationCommands.Redo) { Text = "My Redo" }); } }
}

(note: just wire up the `Window.DataContext` to view-model, as you did)

And two custom classes required for this to wrap.

public class AvalonTextEditor : TextEditor
{
    #region EditText Dependency Property

    public static readonly DependencyProperty EditTextProperty =
        DependencyProperty.Register(
        "EditText",
        typeof(string),
        typeof(AvalonTextEditor),
        new UIPropertyMetadata(string.Empty, EditTextPropertyChanged));
    private static void EditTextPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        AvalonTextEditor editor = (AvalonTextEditor)sender;
        editor.Text = (string)e.NewValue;
    }
    public string EditText
    {
        get { return (string)GetValue(EditTextProperty); }
        set { SetValue(EditTextProperty, value); }
    }

    #endregion

    #region TextEditor Property

    public static TextEditor GetTextEditor(ContextMenu menu) { return (TextEditor)menu.GetValue(TextEditorProperty); }
    public static void SetTextEditor(ContextMenu menu, TextEditor value) { menu.SetValue(TextEditorProperty, value); }
    public static readonly DependencyProperty TextEditorProperty =
        DependencyProperty.RegisterAttached("TextEditor", typeof(TextEditor), typeof(AvalonTextEditor), new UIPropertyMetadata(null, OnTextEditorChanged));
    static void OnTextEditorChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
    {
        ContextMenu menu = depObj as ContextMenu;
        if (menu == null || e.NewValue is DependencyObject == false)
            return;
        TextEditor editor = (TextEditor)e.NewValue;
        NameScope.SetNameScope(menu, NameScope.GetNameScope(editor));
    }

    #endregion

    public AvalonTextEditor()
    {
        this.Loaded += new RoutedEventHandler(AvalonTextEditor_Loaded);
    }

    void AvalonTextEditor_Loaded(object sender, RoutedEventArgs e)
    {
        this.ContextMenu.SetValue(AvalonTextEditor.TextEditorProperty, this);
    }
}

public class AvalonRelayCommand : ICommand
{
    readonly RoutedCommand _routedCommand;
    public string Text { get; set; }
    public AvalonRelayCommand(RoutedCommand routedCommand) { _routedCommand = routedCommand; }
    public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } }
    public bool CanExecute(object parameter) { return _routedCommand.CanExecute(parameter, GetTextArea(GetEditor(parameter))); }
    public void Execute(object parameter) { _routedCommand.Execute(parameter, GetTextArea(GetEditor(parameter))); }
    private AvalonTextEditor GetEditor(object param)
    {
        var contextMenu = param as ContextMenu;
        if (contextMenu == null) return null;
        var editor = contextMenu.GetValue(AvalonTextEditor.TextEditorProperty) as AvalonTextEditor;
        return editor;
    }
    private static TextArea GetTextArea(AvalonTextEditor editor)
    {
        return editor == null ? null : editor.TextArea;
    }
}

Notes:

`EditText` is just a dependency property - to be able to `bind` a Text (your `text`) - that's an Avalon shortcoming. Here just for fun but you may need it, so I left it in.

Use `AvalonRelayCommand` to rewire the Application routed commands - for other stuff use your own Command implementation. Those two classes are the core.

You need to use `AvalonTextEditor` instead of TextEditor - which is just a tiny wrapper - to hook up `ContextMenu` with the `TextEditor` (apart from other problems, menu items are `suffering` from lack of `visual tree` - and you can't get any controls from it that easily). And we need to get a ref to `TextEditor` from the `CommandParameter` (which is set to be a `ContextMenu`). This could've been done with just some attachment properties (w/o overriding the TextEditor), but seems cleaner this way.

On the XAML side - just a few small changes - use the wrapper editor - and you have a `MenuItem` style which `injects` the right parameter for each command (you can do it some other way, this was nicer).

It's not a `hack` - we're just shortcutting the shortcoming of the mouse handling - by manually calling the `TextArea` command handling. That's pretty much it.

Enjoy!

Problem

I have used AvalonEdit control in my project. When I use shortcut keys like Ctrl+C or Ctrl+V, associated copy/paste commands works fine. I decided to use these commands in context menu for more usability because some users get used to right-click instead of shortcut. I used the following XAML code for control: ``` <avalonedit:TextEditor.ContextMenu> <ContextMenu> <MenuItem Command="Undo" /> <MenuItem Command="Redo" /> <Separator/> <MenuItem Command="Cut" /> <MenuItem Command="Copy" /> <MenuItem Command="Paste" /> </ContextMenu> </avalonedit:TextEditor.ContextMenu> ``` but when I run the program these commands are always shown disabled in the context menu as follows: When I first encountered this problem I posted a different question but with the help of MD.Unicorn ( as you see in the comments below) I realized that when you place AvalonEdit in the ItemTemplate of a ListBox or ListView commands doesn't work. With the help of MD.unicorn I created the following testing code to reproduce the result: ViewModel Class and a simple class for data template ``` public class MyViewModel : INotifyPropertyChanged { public MyViewModel() { collection = new ObservableCollection<myClass>(); mc = new myClass(); } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propName) { var h = PropertyChanged; if (h != null) h(this, new PropertyChangedEventArgs(propName)); } public ObservableCollection<myClass> collection { get; set; } public myClass mc { get; set; } } public class myClass { public string text { get; set; } } public partial class MainWindow : Window { MyViewModel _viewModel = new MyViewModel(); public MainWindow() { InitializeComponent(); this.DataContext = _viewModel; } } ``` and XAML code for MainWindow ``` <Window.Resources> <DataTemplate DataType="{x:Type local:myClass}"> <StackPanel> <avalonedit:TextEditor x:Name="xmlMessage" SyntaxHighlighting="XML" ShowLineNumbers="True" > <avalonedit:TextEditor.ContextMenu> <ContextMenu> <MenuItem Command="Undo" /> <MenuItem Command="Redo" /> <Separator/> <MenuItem Command="Cut" /> <MenuItem Command="Copy" /> <MenuItem Command="Paste" /> </ContextMenu> </avalonedit:TextEditor.ContextMenu> </avalonedit:TextEditor> <TextBox Text="test" /> </StackPanel> </DataTemplate> </Window.Resources> <DockPanel> <ListView ItemsSource="{Binding collection}" /> <ContentControl Content="{Binding mc}" /> </DockPanel> ``` If you try this test, you can see that if DataTemplate is used on a content control its command binding in context menu works fine but in ListViewItem they are disabled. Also note that context menu in DataTemplate works fine for TextBox and shows that ListView by itself doesn't inherently breaks the command chain. How can I fix context menu and hook up to control commands in listView items?

Original source

Related problems