I've created RichTextBox control with Menu at the top side of Window. The MenuItems call Commands - It works perfectly. Then I try to create ContextMenu in the RichTextBox and want to call the same commands like in MenuItems.
So that, I bind the ContextMenu in the same way like MenuItems but it throws the NullReferenceException.
What can be the reason. How should I bind the Command to the ContextMenu??
Below are the parts of my code
MenuItem code:
<MenuItem Name="FontSettings" Header="Font settings" Command="{Binding FontSettingsCommand}" CommandParameter="{Binding ElementName=MainRichTbx}" />
RichTextBox code:
<RichTextBox Name="MainRichTbx" TextBlock.LineHeight="0.1" Margin="5" >
<RichTextBox.ContextMenu>
<ContextMenu>
<MenuItem Header="Font settings" Command="{Binding FontSettingsCommand}" CommandParameter="{Binding ElementName=MainRichTbx}" />
</ContextMenu>
</RichTextBox.ContextMenu>
</RichTextBox>
That command which I want to execute:
private ICommand _FontSettingsCommand;
public ICommand FontSettingsCommand
{
get
{
if (_FontSettingsCommand == null)
{
_FontSettingsCommand = new RelayCommand(
argument => EditorFormat.SetFont(argument),
argument => true
);
}
return _FontSettingsCommand;
}
}
The method which I call within the Command:
public static void SetFont(object control)
{
FontDialog fontDialog = new FontDialog();
if (fontDialog.ShowDialog() == DialogResult.OK)
{
(control as System.Windows.Controls.RichTextBox).FontFamily = new System.Windows.Media.FontFamily(fontDialog.Font.Name);
(control as System.Windows.Controls.RichTextBox).FontSize = fontDialog.Font.Size;
(control as System.Windows.Controls.RichTextBox).FontStyle = fontDialog.Font.Italic ? FontStyles.Italic : FontStyles.Normal;
(control as System.Windows.Controls.RichTextBox).FontWeight = fontDialog.Font.Bold ? FontWeights.Bold : FontWeights.Regular;
}
}
And The RelayCommand class
class RelayCommand : ICommand
{
private readonly Action<object> _Execute;
private readonly Func<object, bool> _CanExecute;
public RelayCommand(Action<object> execute, Func<object, bool> canExecute)
{
if (execute == null) throw new ArgumentNullException("execute");
_Execute = execute;
_CanExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return _CanExecute == null ? true : _CanExecute(parameter);
}
public event EventHandler CanExecuteChanged
{
add
{
if (_CanExecute != null) CommandManager.RequerySuggested += value;
}
remove
{
if (_CanExecute != null) CommandManager.RequerySuggested -= value;
}
}
public void Execute(object parameter)
{
_Execute(parameter);
}
}
I have a solution which I think works, binding to the PlacementTarget of the context menu.
<RichTextBox Name="MainRichTbx" TextBlock.LineHeight="0.1" Margin="5" >
<RichTextBox.ContextMenu>
<ContextMenu>
<MenuItem Header="Font settings"
Command="{Binding FontSettingsCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}, Path=PlacementTarget}" />
</ContextMenu>
</RichTextBox.ContextMenu>
</RichTextBox>
However, the XAML designer underlines the CommandPamameter and shows the tooltip "RelativeSource is not in FindAncestor mode". Nevertheless it seems to work.
Edit
Adding Mode=FindAncestor seems to fix the error message. I don't know if it has any effect on the behavior.
<RichTextBox Name="MainRichTbx" TextBlock.LineHeight="0.1" Margin="5" >
<RichTextBox.ContextMenu>
<ContextMenu>
<MenuItem Header="Font settings"
Command="{Binding FontSettingsCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ContextMenu}}, Path=PlacementTarget}" />
</ContextMenu>
</RichTextBox.ContextMenu>
</RichTextBox>
This is probably because the binding can't find your text box.
This happens because the ContextMenu is not part of the visual tree so can't find your Text Box for the CommandParameter.
The best way around this is to not use the CommandParameter at all, but use a variable in your ViewModel (such as SelectedTextBox).
However, you can get a working (but slightly uglier) solution by naming your ContextMenu and setting its NameScope in the View's constructor:
NameScope.SetNameScope(myContextMenu, this);
This should then work correctly.
Related
I'm using Josh Smith RelayCommand class in a WPF MVVM app for creating my commands inside my ViewModel:
For example:
ICommand RemoveAllCommand = new RelayCommand<object>(OnRemoveAll, CanRemoveAll);
I am calling this command from a ContextMenu:
<ContextMenu x:Key="MyContextMenu" DataContext="{Binding Path=PlacementTarget, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Remove All" Command="{Binding Path=DataContext.RemoveAllCommand,
RelativeSource={RelativeSource AncestorType={x:Type TreeView}}}" CommandParameter="{Binding Path=.Header}" />
Everything works fine, except that my MenuItem's is still visible but disabled, I would like to set the Visibility to Collapsed so that my MenuItem doesn't show up when CanExecute in Relay Command returns false.
I tried to set a binding to Visibility property, but I don't know how to bind to my CanRemoveAll(object obj) method with parameter. I also thought about using a DataTrigger but I'm not sure how to do it.
Here is my CanRemoveAll method in the ViewModel:
public bool CanRemoveAll(object param)
{
GoldTreeNodeViewModel gtn = param as GoldTreeNodeViewModel;
return (gtn != null && gtn.Children != null && gtn.Children.Count > 0);
}
From the RelayCommand class:
public event EventHandler CanExecuteChanged
{
add
{
if (_canExecute != null)
CommandManager.RequerySuggested += value;
}
remove
{
if (_canExecute != null)
CommandManager.RequerySuggested -= value;
}
}
[DebuggerStepThrough]
public Boolean CanExecute(Object parameter)
{
return _canExecute == null ? true : _canExecute((T) parameter);
}
Any help will be highly appreciated,
<ContextMenu x:Key="MyContextMenu" DataContext="{Binding Path=PlacementTarget, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Remove All" Command="{Binding Path=DataContext.RemoveAllCommand,
RelativeSource={RelativeSource AncestorType={x:Type TreeView}}}" CommandParameter="{Binding Path=.Header}"
Visibility="{Binding DataContext.RemoveVisibility,RelativeSource={RelativeSource AncestorType={x:Type TreeView}}}"
/>
private Visibility _removeVisibility;
public Visibility RemoveVisibility
{
get { return _removeVisibility; }
set { _removeVisibility = value; Notify("RemoveVisibility"); }
}
public bool CanRemoveAll(object param)
{
GoldTreeNodeViewModel gtn = param as GoldTreeNodeViewModel;
bool result= (gtn != null && gtn.Children != null && gtn.Children.Count > 0);
if (result)
RemoveVisibility = Visibility.Visible;
else
RemoveVisibility = Visibility.Collapsed;
return result;
}
I think that DataContext you have binded correspond to your ViewModel . I hope this will help.
Pressing the delete key is not causing the command below to fire. One can assume DelegateCommand follows interface contracts since other instances of DelegateCommand referenced in the View are properly firing within the ViewModel.
Why?
View:
<ListBox Height="132"
Name="lbFiles"
ItemsSource="{Binding LbItems, UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True}"
VerticalAlignment="Center"
SelectedValue="{Binding Path=ListBoxSelection}">
<ListBox.InputBindings>
<KeyBinding Key="Delete"
Command="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}
,Path=DataContext.CommandInputFilesDeleteSelected}" />
</ListBox.InputBindings>
ViewModel:
private DelegateCommand commandInputFilesDeleteSelected;
public ICommand CommandInputFilesDeleteSelected {
get {
if (commandInputFilesDeleteSelected == null) {
commandInputFilesDeleteSelected = new DelegateCommand(InputFilesDeleteSelected);
}
return saveCommand;
}
}
private void InputFilesDeleteSelected() { //this never fires :(
LbItems.Remove(ListBoxSelection);
}
private DelegateCommand commandInputFilesDeleteSelected;
public ICommand CommandInputFilesDeleteSelected {
get {
if (commandInputFilesDeleteSelected == null) {
commandInputFilesDeleteSelected = new DelegateCommand(InputFilesDeleteSelected);
}
return saveCommand; //changed from saveCommand to commandInputFilesDeleteSelected whoops
}
}
I'm using the MVVM Light Toolkit with Silverlight.
On my UserControl I have a ListBox that displays a list of files. Each file has a delete image next to the file name. In the DataTemplate for the listbox I have an image (or can use a button) and a TextBlock.
So I want to capture using the event when the user will clicks on the image(or button with image) to remove the file from the list of files.
But I cannot seem to capture the event. Maybe this is due to having the SelectedItem Event on the listbox?
public class MainViewModel : ViewModelBase
{
#region Properties
public const string SelectedListBoxFilePropertyName = "SelectedUploadFile";
private UploadFile _selectedUploadFile = null;
public UploadFile SelectedUploadFile
{
get
{
return _selectedUploadFile;
}
set
{
if (_selectedUploadFile == value)
return;
_selectedUploadFile = value;
RaisePropertyChanged(SelectedListBoxFilePropertyName);
}
}
public const string UploadFilesPropertyName = "UploadFiles";
private ObservableCollection<UploadFile> _uploadFiles = new ObservableCollection<UploadFile>();
public ObservableCollection<UploadFile> UploadFiles
{
get
{
return _uploadFiles;
}
set
{
if (_uploadFiles == value)
return;
_uploadFiles = value;
RaisePropertyChanged(UploadFilesPropertyName);
}
}
#endregion
public static ICommand BrowseCommand { get; private set; }
public static ICommand DragDropFileCommand { get; private set; }
public static ICommand RemoveCommand { get; private set; }
#region Constructor
public MainViewModel()
{
if (IsInDesignMode)
{
// Code runs in Blend --> create design time data.
UploadFiles = new UploadFileContainer().UploadFiles;
}
else
{
// Code runs "for real"
}
WireUpCommands();
}
#endregion
#region Event Handlers
private void OnBrowseFileCommand()
{
var dialog = new OpenFileDialog();
dialog.ShowDialog();
if (dialog.Files != null)
AddFiles(dialog.Files);
}
private void OnDropFileCommand(DragEventArgs e)
{
var files = e.Data.GetData(DataFormats.FileDrop) as FileInfo[];
AddFiles(files);
}
private void OnRemoveFileCommand()
{
UploadFiles.Remove(_selectedUploadFile);
}
#endregion
#region Private Methods
private void WireUpCommands()
{
BrowseCommand = new RelayCommand(OnBrowseFileCommand);
DragDropFileCommand = new RelayCommand<DragEventArgs>(e => OnDropFileCommand(e));
RemoveCommand = new RelayCommand(OnRemoveFileCommand);
UploadCommand = new RelayCommand(OnClickUploadCommand);
}
#endregion
}
<ListBox Grid.Row="1" Height="214" HorizontalAlignment="Left" AllowDrop="True" Margin="6,26,0,0" Name="UploadFilesListBox" VerticalAlignment="Top" Width="415" ItemsSource="{Binding Path=UploadFiles}" SelectedItem="{Binding Path=SelectedListBoxFile, Mode=TwoWay}" ScrollViewer.VerticalScrollBarVisibility="Auto">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Drop">
<cmd:EventToCommand Command="{Binding DragDropFileCommand}" PassEventArgsToCommand="True"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<ListBox.Background>
<ImageBrush ImageSource="/FileUploadApplication;component/Resources/dragdrophere.png" Stretch="None" />
</ListBox.Background>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Button Command="{Binding RemoveCommand}">
<Image Source="/FileUploadApplication;component/Resources/delete.png"/>
</Button>
<Image Source="/FileUploadApplication;component/Resources/delete.png">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<cmd:EventToCommand Command="{Binding RemoveCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Image> <TextBlock Text=" " />
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Since your ItemsSource is UploadFiles it's probably sending the event to UploadFile and not the view model the user control is bound to.
Your button is the element of the ItemTemplate. you're binding the listbox ItemsSource to the ObservableCollection. Every Itemtemplate DataContext is no MainViewModel, but UploadFile, which has no RemoveCommand.
I was solving this by adding to every item the parent object using constructor. RemoveCommand was inside the item's ViewModel and insede the remove function i was calling the parent's method to delete the item.
Not sure if that's the best solution but it worked for me.
I'm currently taking a good look at the excellent toolkit from Laurent and I have the following question.
From Blend 4, I have added an EventTrigger for the Loaded event, in my ViewModel I have the following:
public RelayCommand rcAutoGeneratingColumn { get; private set; }
In the constructor I have:
rcAutoGeneratingColumn =
new RelayCommand(o => DataGridAutoGeneratingColumn(o));
Also in the ViewModel, I have the method which I wish to be invoked by the RelayCommand:
private void DataGridAutoGeneratingColumn(Object o)
{
DataGrid grid = (DataGrid)o;
foreach (DataGridTextColumn col in grid.Columns)
{
if (col.Header.ToString().ToLower() == "id")
{
col.Visibility = System.Windows.Visibility.Hidden;
}
}
}
My XAML contains the following (for the DataGrid):
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<GalaSoft_MvvmLight_Command:EventToCommand
Command="{Binding rcAutoGeneratingColumn, Mode=OneWay}"
CommandParameter="{Binding ElementName=dataGrid1, Mode=OneWay}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
There is NO PROBLEM here the code works just fine, but obviously the event used to hide certain columns should be the AutoGeneratingColumn event and not Loaded.
I have used to Loaded event as a getaround.
I was hoping that I could relay any event offered by the control so that, in this case, the following would work instead:
<i:Interaction.Triggers>
<i:EventTrigger EventName="AutoGeneratingColumn">
<GalaSoft_MvvmLight_Command:EventToCommand
Command="{Binding rcAutoGeneratingColumn, Mode=OneWay}"
CommandParameter="{Binding ElementName=dataGrid1, Mode=OneWay}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
I am unable to get the AutoGeneratingColumn event to trigger, and I'm hoping that I've overlooked something and appreciate any advice given!
This behaviour is the same with the GridControl from DevExpress, in that the Loaded event is triggered whereas the ColumnsPopulated event (this being the equivalent of the AutoGeneratingColumn event) is not.
DevExpress offered the following information with regard to my question:
"We have reviewed this question, and come to an interesting conclusion. It looks like the visual tree is not being built at the moment when the Interaction.Triggers are being processed"
If this is true, and there is no other way in which to invoke the events within the ViewModel, then one would have to go ahead and - by using trial and error - note which of the DataGrid events (of which there are over 100) can be invoked in this way and which cannot!
One would like to think that every event which is available in the code-behind, can also be reached when applying the MVVM pattern.
I have searched for an answer but I cannot rule out that I have overlooked something, so if this is to be the case, then please accept my apologies!
You don't have to use evil code behind ;-) You can do this using an attached behaviour...
public class AutoGeneratingColumnEventToCommandBehaviour
{
public static readonly DependencyProperty CommandProperty =
DependencyProperty.RegisterAttached(
"Command",
typeof(ICommand),
typeof(AutoGeneratingColumnEventToCommandBehaviour),
new PropertyMetadata(
null,
CommandPropertyChanged));
public static void SetCommand(DependencyObject o, ICommand value)
{
o.SetValue(CommandProperty, value);
}
public static ICommand GetCommand(DependencyObject o)
{
return o.GetValue(CommandProperty) as ICommand;
}
private static void CommandPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var dataGrid = d as DataGrid;
if (dataGrid != null)
{
if (e.OldValue != null)
{
dataGrid.AutoGeneratingColumn -= OnAutoGeneratingColumn;
}
if (e.NewValue != null)
{
dataGrid.AutoGeneratingColumn += OnAutoGeneratingColumn;
}
}
}
private static void OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
var dependencyObject = sender as DependencyObject;
if (dependencyObject != null)
{
var command = dependencyObject.GetValue(CommandProperty) as ICommand;
if (command != null && command.CanExecute(e))
{
command.Execute(e);
}
}
}
}
Then use it in XAML like this...
<DataGrid ItemsSource="{Binding MyGridSource}"
AttachedCommand:AutoGeneratingColumnEventToCommandBehaviour.Command="{Binding CreateColumnsCommand}">
</DataGrid>
Just set EventTrigger.SourceObject property.
<DataGrid
x:Name="DataGrid"
AutoGenerateColumns="True"
IsReadOnly="True"
ItemsSource="{Binding Data}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="AutoGeneratingColumn" SourceObject="{Binding ElementName=DataGrid}">
<local:EventToCommand
Command="{Binding ColumnGeneratingCommand}"
PassEventArgsToCommand="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
</DataGrid>
As MVVMLight from Galasoft is deprecated now, we can use CommunityToolkit.Mvvm package and use it like this:
<DataGrid AutoGenerateColumns="True"
Name="DataGrid"
ItemsSource="{Binding Items}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="AutoGeneratingColumn" SourceObject="{Binding ElementName=DataGrid}">
<i:InvokeCommandAction Command="{Binding AutoGeneratingColumnCommand}" PassEventArgsToCommand="True"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</DataGrid>
Note that Items property is a simple List, It could be an ObservableCollection or whatever.
The trick to get the fired event is to load your data after the window is loaded, or raise OnpropertyChanged on Items property after loaded.
<Window ...>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding LoadedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Window>
In your View Model:
private RelayCommand<DataGridAutoGeneratingColumnEventArgs> myAutoGeneratingColumnCommand;
public RelayCommand<DataGridAutoGeneratingColumnEventArgs> AutoGeneratingColumnCommand
{
get
{
if (myAutoGeneratingColumnCommand == null)
myAutoGeneratingColumnCommand = new RelayCommand<DataGridAutoGeneratingColumnEventArgs>(AutoGeneratingColumnCommandAction);
return myAutoGeneratingColumnCommand;
}
}
private void AutoGeneratingColumnCommandAction(DataGridAutoGeneratingColumnEventArgs e)
{
if (e.PropertyName == "Id")
{
e.Column.Width = 60;
}
else if (e.PropertyName == "Name")
{
e.Column.Header = "myName";
e.Column.Width = new DataGridLength(1, DataGridLengthUnitType.Star);
}
else
e.Cancel = true; // ignore all other properties and remove their column
}
RelayCommand myLoadedCommand;
public RelayCommand LoadedCommand
{
get
{
if (myLoadedCommand == null)
myLoadedCommand = new RelayCommand(LoadedCommandAction);
return myLoadedCommand;
}
}
private void LoadedCommandAction()
{
Load(); // Populate the Items List
}
During the course of developing a project with MVVM you're going to have circumstances where you must handle events in your view's code-behind and EventToCommand just plain doesn't work. You especially find this with Silverlight, but I assume from your question that you're using WPF. It's okay to do some event handling in your view's code-behind, just don't put any business logic there. You can even leave the command in your view model, just call it directly from your event handler.
((YourViewModel)this.DataContext).rcAutoGeneratingColumn.Execute(sender);
in VM, set ICommand like:
private RelayCommand<EventArgs> _myCommand = null;
public RelayCommand<EventArgs> MyCommand
{
get
{
if (_myCommand == null)
{
_myCommand = new RelayCommand<EventArgs>((e) =>
{
//....
}
);
}
return _myCommand;
}
}
In xaml, binding to this command like
<Button Content="Test Command" Margin="2,0,2,0" Command="{Binding Path=MyCommand}" CommandParameter="{Binding ElementName=InputTextBox, Path=Text}" />
then run the app. it say can't convert string to EventArgs.
How to set EventArgs for ICommand binding?
I'm not familiar with RelayCommand<T> I've only come across RelayCommand.
However it would seem that RelayCommand<EventArgs> can't possibly be useful. I suspect that you would at least need RelayCommand<string>.