0

I have a ContextMenu that suppose to set value on its parent TextBox.

enter image description here

The textbox cannot have a name (by requirement), so I am setting it as CommandTarget

    <TextBox Text="{Binding TextBoxOne, UpdateSourceTrigger=LostFocus}">
        <TextBox.ContextMenu>
            <ContextMenu>
                <MenuItem Header="Set to 35"
                          Command="{Binding SetAmountCommand}"
                          CommandParameter="35"
                          CommandTarget="{Binding Text, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TextBox}}}" />
                <MenuItem Header="Set to 50"
                          Command="{Binding SetAmountCommand}"
                          CommandParameter="50"
                          CommandTarget="{Binding Text, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TextBox}}}" />
            </ContextMenu>
        </TextBox.ContextMenu>

How to access the TextBox.Text from inside the Command ?

ViewModel

public class MainVm : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    public string TextBoxOne { get; set; } = "One";

    private ICommand _setAmountCommand;
    public ICommand SetAmountCommand
    {
        get
        {
            return _setAmountCommand ?? (_setAmountCommand = new CommandParameterHandler((o) =>
            {
                object param = o;
                double amount = (double)o;
                //MyParentTextBox.Text = amount; //What to put here ? (Cannot be TextBoxOne = amount, need to route from View)
            }, true));
        }
    }
}

Generic CommandParameterHandler

public class CommandParameterHandler : ICommand
{
    private Action<object> _action;
    private bool _canExecute;
    public CommandParameterHandler(Action<object> action, bool canExecute)
    {
        _action = action;
        _canExecute = canExecute;
    }
    public bool CanExecute(object parameter)
    {
        return _canExecute;
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        _action(parameter);
    }
}
10
  • The CommandTarget property is only applicable when the ICommand is a RoutedCommand, which is not true in your case. Commented May 7, 2018 at 14:01
  • Why can't you use the binding? Can you please be more specific on that? Commented May 7, 2018 at 14:08
  • Your RelativeSource doesn't work. See here stackoverflow.com/questions/15550240/… Commented May 7, 2018 at 14:11
  • What is the purpose of implementing INotifyPropertyChanged and MVVM pattern if you do not notify your view about changes? Commented May 7, 2018 at 14:11
  • 1
    @JesonMartajaya you need to change your bindings from RelativeSource to something depending on PlacementTarget, like this <MenuItem Command="CMD" CommandTarget="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=ContextMenu}, Path=PlacementTarget}">. This makes CommandTarget value be bound to an element the menu is open on. If you need DataContext, just add one more binding. Commented May 7, 2018 at 14:31

2 Answers 2

1

You can only pass one CommandParameter to the command. If you want to pass is something in addition to the actual value, you could create a custom composite type that carries more than one value:

public class CompositeParameter : Freezable
{
    protected override Freezable CreateInstanceCore()
    {
        return this;
    }

    public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(nameof(Value), 
        typeof(string), typeof(CompositeParameter));

    public string Value
    {
        get { return (string)GetValue(ValueProperty); }
        set { SetValue(ValueProperty, value); }
    }

    public static readonly DependencyProperty ControlProperty = DependencyProperty.Register(nameof(Control),
        typeof(FrameworkElement), typeof(CompositeParameter));
    public FrameworkElement Control
    {
        get { return (FrameworkElement)GetValue(ControlProperty); }
        set { SetValue(ControlProperty, value); }
    }
}

View Model:

public ICommand SetAmountCommand
{
    get
    {
        return _setAmountCommand ?? (_setAmountCommand = new CommandParameterHandler((o) =>
        {
            CompositeParameter param = o as CompositeParameter;
            if (param != null)
            {
                double amount = Convert.ToDouble(param.Value);
                //...
                TextBox textBox = param.Control as TextBox;
                if (textBox != null)
                    textBox.Text = param.Value;
            }
        }, true));
    }
}

View:

<TextBox Text="{Binding TextBoxOne, UpdateSourceTrigger=LostFocus}">
    <TextBox.ContextMenu>
        <ContextMenu>
            <ContextMenu.Resources>
                <local:CompositeParameter x:Key="paramA"
                                          Value="35" 
                                          Control="{Binding PlacementTarget, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
                <local:CompositeParameter x:Key="paramB"
                                          Value="50" 
                                          Control="{Binding PlacementTarget, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
            </ContextMenu.Resources>
            <MenuItem Header="Set to 35"
                      Command="{Binding SetAmountCommand}" 
                      CommandParameter="{StaticResource paramA}" />
            <MenuItem Header="Set to 50"
                      Command="{Binding SetAmountCommand}"
                      CommandParameter="{StaticResource paramB}" />
        </ContextMenu>
    </TextBox.ContextMenu>
</TextBox>
Sign up to request clarification or add additional context in comments.

1 Comment

Interesting workaround to pass CommandTarget as CommandParameter. Thank you.
0

After 2 days searching for answer, I came across this RoutedCommand tutorial. Yes, you can access CommandTarget from Command, but it has to be a static RoutedCommand. This approach fits the need as SetAmountCommand is shared by multiple MenuItem.

XAML

<Window x:Class="WpfCommandTargetDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfCommandTargetDemo">
    <Window.CommandBindings>
        <CommandBinding CanExecute="SetAmountCommand_CanExecute"
                        Command="{x:Static local:CustomRoutedCommand.SetAmountCommand}"
                        Executed="SetAmountCommand_Executed" />
    </Window.CommandBindings>
    <StackPanel>
        <TextBox Text="{Binding TextBoxOne, UpdateSourceTrigger=LostFocus}">
            <TextBox.ContextMenu>
                <ContextMenu>
                    <MenuItem Header="Set to 35"
                              Command="{x:Static local:CustomRoutedCommand.SetAmountCommand}"
                              CommandParameter="35"
                              CommandTarget="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=ContextMenu}, Path=PlacementTarget}" />
                    <MenuItem Header="Set to 50"
                              Command="{x:Static local:CustomRoutedCommand.SetAmountCommand}"
                              CommandParameter="50"
                              CommandTarget="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=ContextMenu}, Path=PlacementTarget}" />
                </ContextMenu>
            </TextBox.ContextMenu>
        </TextBox>
    </StackPanel>
</Window>

CodeBehind

public partial class MainWindow : Window
{
    private readonly MainVm _mainVm;

    public MainWindow()
    {
        InitializeComponent();
        _mainVm = new MainVm();
        DataContext = _mainVm;
    }

    void SetAmountCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
        e.CanExecute = true;
    }

    void SetAmountCommand_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        object param = e.Parameter; //CommandParameter
        TextBox textbox = e.OriginalSource as TextBox; //CommandTarget
        if (textbox != null)
        {
            textbox.Text = param.ToString();
        }
    }
}

RoutedCommand has to be static, because it is statically bound to XAML element.

public static class CustomRoutedCommand
{
    public static readonly RoutedCommand SetAmountCommand = new RoutedCommand();
}

For completeness, I cannot have the Command on my ViewModel. SetAmountCommand property is removed.

public class MainVm : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    public string TextBoxOne { get; set; } = "One";
}

2 Comments

So you want to handle the logic in the view? Then you don't need to use commands. You might as well handle the Click event of the MenuItem.
I need the command to be passed from ViewModel, and the MenuItem is generated and sorted dynamically. This command also changes the Text on the view only while keeping the ViewModel property unchanged until next LostFocus.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.