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!
avalonedit:TextEditor
that you omitted. – AlejoaText
property to show text, but I used a dependency property to use binding for showing control content, according to this post, so I must missing something that is breaking the command binding. Do you have any idea? – Airboatpublic class myClass{public string text { get; set; }}
then add aObservableCollection<MyClass>
property in viewModel and bind that property to ItemSource of a ListView or ListBox. add a DataTemplate forMyClass
type and place an avalonedit control in it. – Airboat