Question

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:

screenshot of context menu

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?

Was it helpful?

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!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top