Chris
Chris

Reputation: 363

Binding to a command in code while passing command parameters

I recently implemented a solution into my code that allows me to bind to my command in my view model. Here is a link to the method that I used: https://code.msdn.microsoft.com/Event-to-Command-24d903c8. I used the 2nd method in the link. You can assume for all intents and purposes that my code is very similar to this code. This works just fine. However, I need to bind a command parameter for this double click as well. How would I set that up?

Here is some background on my project. Some of the setup behind this project may seem odd, but it must be done in this way, due to a bunch of details that I won't get into here. The first thing to note would be that this binding setup is happening inside of a multivalue converter. Here is my code generating the new element:

DataTemplate dt = new DataTemplate();
dt.DataType = typeof(Button);

FrameworkElementFactory btn = new FrameworkElementFactory(typeof(Button));
btn.SetValue(Attached.DoubleClickCommandProperty, ((CardManagementViewModel)values[1]).ChangeImageCommand);

dt.VisualTree = btn;

values[1] is the DataContext, which is the viewmodel here. The View Model contains the following:

private RelayCommand _ChangeImageCommand;

public ICommand ChangeImageCommand
{
    get
    {
        if (_ChangeImageCommand == null)
        {
            _ChangeImageCommand = new RelayCommand(
                param => this.ChangeImage(param)
                );
        }
        return _ChangeImageCommand;
    }
}

private void ChangeImage(object cardParam)
{
}

How can I pass that command parameter? I have bound all this stuff using XAML many times before, but have never had to do it from C#. Thank you for any and all help!

EDIT

Here is a complete sample of my issue. Though I know that this sample has no practical purpose to do things the way it does, for the sake of this problem, we will just run with it.

Let's say that I have an ObservableCollection of strings that I want to show. These are contained in viewmodel.

private ObservableCollection<string> _MyList;
public ObservableCollection<string> MyList { get { return _MyList; } set { if (_MyList != value) { _MyList = value; RaisePropertyChanged("MyList"); } } }
public ViewModel()
{
    MyList = new ObservableCollection<string>();
    MyList.Add("str1");
    MyList.Add("str2");
    MyList.Add("str3");
}

So the guy in charge of the UI on my team hands me this

<ContentControl>
    <ContentControl.Content>
        <MultiBinding Converter="{StaticResource ResourceKey=MyConverter}">
            <Binding Path="MyList"/>
            <Binding />
        </MultiBinding>
    </ContentControl.Content>
</ContentControl>

Now lets say that the UI person and my project manager decide to conspire against me to make my life a living hell, so they tell me that I need to create a listbox to display these items as buttons, not in the XAML, but in the converter that the ContentControl's Content is bound to. So I do this:

public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
    ListBox lb = new ListBox();
    lb.ItemsSource = (ObservableCollection<string>)values[0];
    DataTemplate dt = new DataTemplate();
    dt.DataType = typeof(Button);

    FrameworkElementFactory btn = new FrameworkElementFactory(typeof(Button));
    btn.SetValue(Button.WidthProperty, 100D);
    btn.SetValue(Button.HeightProperty, 50D);
    btn.SetBinding(Button.ContentProperty, new Binding());

    dt.VisualTree = btn;
    lb.ItemTemplate = dt;

    return lb;
}

This successful displays the listbox, with all the items as buttons. The next day, my idiot project manager creates a new command in the view model. It's purpose is to add the selected item in the listbox if one of the button's is double clicked. NOT SINGLE CLICKED, BUT DOUBLE CLICKED! This means that I can't use the CommandProperty or, more importantly, the CommandParameterProperty. His Command in the viewmodel looks something like this:

private RelayCommand _MyCommand;

public ICommand MyCommand
{
    get
    {
        if (_MyCommand == null)
        {
            _MyCommand = new RelayCommand(
                param => this.MyMethod(param)
                );
        }
        return _MyCommand;
    }
}

private void MyMethod(object myParam)
{
    MyList.Add(myParam.ToString());
}

So after some googling, I find a class that turns my DoubleClick event into an attached property. Here is that class:

public class Attached
{
    static ICommand command;

    public static ICommand GetDoubleClickCommand(DependencyObject obj)
    {
        return (ICommand)obj.GetValue(DoubleClickCommandProperty);
    }

    public static void SetDoubleClickCommand(DependencyObject obj, ICommand value)
    {
        obj.SetValue(DoubleClickCommandProperty, value);
    }

    // Using a DependencyProperty as the backing store for DoubleClickCommand.  This enables animation, styling, binding, etc... 
    public static readonly DependencyProperty DoubleClickCommandProperty =
        DependencyProperty.RegisterAttached("DoubleClickCommand", typeof(ICommand), typeof(Attached), new UIPropertyMetadata(null, CommandChanged));

    static void CommandChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        var fe = obj as FrameworkElement;
        command = e.NewValue as ICommand;
        fe.AddHandler(Button.MouseDoubleClickEvent, new RoutedEventHandler(ExecuteCommand));
    }

    static void ExecuteCommand(object sender, RoutedEventArgs e)
    {
        var ele = sender as Button;
        command.Execute(null);
    }
}

Back in the converter then, I put this line right above dt.VisualTree = btn;:

btn.SetValue(Attached.DoubleClickCommandProperty, ((ViewModel)values[1]).MyCommand);

This successfully hits my project manager's command, but I still need to pass the listbox's selected item. My Project Manager then tells me that I am not allowed to touch the viewmodel anymore. This is where I am stuck. How can I still send the listbox's selected item to my project manager's command in the view model?

Here is the full code files for this example:

ViewModel.cs

using System.Collections.ObjectModel;
using System.Windows.Input;
using WpfApplication2.Helpers;

namespace WpfApplication2
{
    public class ViewModel : ObservableObject
    {
        private ObservableCollection<string> _MyList;
        private RelayCommand _MyCommand;

        public ObservableCollection<string> MyList { get { return _MyList; } set { if (_MyList != value) { _MyList = value; RaisePropertyChanged("MyList"); } } }

        public ViewModel()
        {
            MyList = new ObservableCollection<string>();
            MyList.Add("str1");
            MyList.Add("str2");
            MyList.Add("str3");
        }

        public ICommand MyCommand
        {
            get
            {
                if (_MyCommand == null)
                {
                    _MyCommand = new RelayCommand(
                        param => this.MyMethod(param)
                        );
                }
                return _MyCommand;
            }
        }

        private void MyMethod(object myParam)
        {
            MyList.Add(myParam.ToString());
        }
    }
}

MainWindow.xaml

<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:helpers="clr-namespace:WpfApplication2.Helpers"
        xmlns:Converters="clr-namespace:WpfApplication2.Helpers.Converters"
        xmlns:local="clr-namespace:WpfApplication2"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:ViewModel/>
    </Window.DataContext>
    <Window.Resources>
        <Converters:MyConverter x:Key="MyConverter"/>
    </Window.Resources>
    <ContentControl>
        <ContentControl.Content>
            <MultiBinding Converter="{StaticResource ResourceKey=MyConverter}">
                <Binding Path="MyList"/>
                <Binding />
            </MultiBinding>
        </ContentControl.Content>
    </ContentControl>
</Window>

MyConverter.cs

using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace WpfApplication2.Helpers.Converters
{
    public class MyConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            ListBox lb = new ListBox();
            lb.ItemsSource = (ObservableCollection<string>)values[0];

            DataTemplate dt = new DataTemplate();
            dt.DataType = typeof(Button);

            FrameworkElementFactory btn = new FrameworkElementFactory(typeof(Button));
            btn.SetValue(Button.WidthProperty, 100D);
            btn.SetValue(Button.HeightProperty, 50D);
            btn.SetBinding(Button.ContentProperty, new Binding());

            btn.SetValue(Attached.DoubleClickCommandProperty, ((ViewModel)values[1]).MyCommand);
            // Somehow create binding so that I can pass the selected item of the listbox to the 
            // above command when the button is double clicked.  

            dt.VisualTree = btn;
            lb.ItemTemplate = dt;

            return lb;
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

Attached.cs

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace WpfApplication2.Helpers
{
    public class Attached
    {
        static ICommand command;

        public static ICommand GetDoubleClickCommand(DependencyObject obj)
        {
            return (ICommand)obj.GetValue(DoubleClickCommandProperty);
        }

        public static void SetDoubleClickCommand(DependencyObject obj, ICommand value)
        {
            obj.SetValue(DoubleClickCommandProperty, value);
        }

        // Using a DependencyProperty as the backing store for DoubleClickCommand.  This enables animation, styling, binding, etc... 
        public static readonly DependencyProperty DoubleClickCommandProperty =
            DependencyProperty.RegisterAttached("DoubleClickCommand", typeof(ICommand), typeof(Attached), new UIPropertyMetadata(null, CommandChanged));

        static void CommandChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var fe = obj as FrameworkElement;
            command = e.NewValue as ICommand;
            fe.AddHandler(Button.MouseDoubleClickEvent, new RoutedEventHandler(ExecuteCommand));
        }

        static void ExecuteCommand(object sender, RoutedEventArgs e)
        {
            var ele = sender as Button;
            command.Execute(null);
        }
    }
}

ObservableObject.cs

using System;
using System.ComponentModel;
using System.Diagnostics;

namespace WpfApplication2.Helpers
{
    public class ObservableObject : INotifyPropertyChanged
    {
        #region Debugging Aides

        /// <summary>
        /// Warns the developer if this object does not have
        /// a public property with the specified name. This 
        /// method does not exist in a Release build.
        /// </summary>
        [Conditional("DEBUG")]
        [DebuggerStepThrough]
        public virtual void VerifyPropertyName(string propertyName)
        {
            // Verify that the property name matches a real,  
            // public, instance property on this object.
            if (TypeDescriptor.GetProperties(this)[propertyName] == null)
            {
                string msg = "Invalid property name: " + propertyName;

                if (this.ThrowOnInvalidPropertyName)
                    throw new Exception(msg);
                else
                    Debug.Fail(msg);
            }
        }

        /// <summary>
        /// Returns whether an exception is thrown, or if a Debug.Fail() is used
        /// when an invalid property name is passed to the VerifyPropertyName method.
        /// The default value is false, but subclasses used by unit tests might 
        /// override this property's getter to return true.
        /// </summary>
        protected virtual bool ThrowOnInvalidPropertyName { get; private set; }

        #endregion // Debugging Aides

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Raises the PropertyChange event for the property specified
        /// </summary>
        /// <param name="propertyName">Property name to update. Is case-sensitive.</param>
        public virtual void RaisePropertyChanged(string propertyName)
        {
            this.VerifyPropertyName(propertyName);
            OnPropertyChanged(propertyName);
        }

        /// <summary>
        /// Raised when a property on this object has a new value.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Raises this object's PropertyChanged event.
        /// </summary>
        /// <param name="propertyName">The property that has a new value.</param>
        protected virtual void OnPropertyChanged(string propertyName)
        {
            this.VerifyPropertyName(propertyName);

            PropertyChangedEventHandler handler = this.PropertyChanged;
            if (handler != null)
            {
                var e = new PropertyChangedEventArgs(propertyName);
                handler(this, e);
            }
        }

        #endregion // INotifyPropertyChanged Members
    }
}

RelayCommand.cs

using System;
using System.Diagnostics;
using System.Windows.Input;

namespace WpfApplication2.Helpers
{
    public class RelayCommand : ICommand
    {
        #region Fields

        readonly Action<object> _execute;
        readonly Predicate<object> _canExecute;

        #endregion // Fields

        #region Constructors

        /// <summary>
        /// Creates a new command that can always execute.
        /// </summary>
        /// <param name="execute">The execution logic.</param>
        public RelayCommand(Action<object> execute)
            : this(execute, null)
        {
        }

        /// <summary>
        /// Creates a new command.
        /// </summary>
        /// <param name="execute">The execution logic.</param>
        /// <param name="canExecute">The execution status logic.</param>
        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
        {
            if (execute == null)
                throw new ArgumentNullException("execute");

            _execute = execute;
            _canExecute = canExecute;
        }

        #endregion // Constructors

        #region ICommand Members

        [DebuggerStepThrough]
        public bool CanExecute(object parameters)
        {
            return _canExecute == null ? true : _canExecute(parameters);
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameters)
        {
            _execute(parameters);
        }

        #endregion // ICommand Members
    }
}

Again, thank you for any help!!!

Upvotes: 1

Views: 3082

Answers (1)

Peter Duniho
Peter Duniho

Reputation: 70652

The code you posted isn't actually a minimal or complete example. At the very least, it's missing the CardManagementViewModel type, and of course the example appears to be based on the original code, with no attempt to reduce it to a minimal example.

As such, I did not spend much time looking through all of the code, never mind did I bother to compile and run it. However, the main thing that was missing in your original edit is the implementation of the attached property. So with that in hand, I propose you change your Attached class so it looks like this:

public class Attached
{
    public static ICommand GetDoubleClickCommand(DependencyObject obj)
    {
        return (ICommand)obj.GetValue(DoubleClickCommandProperty);
    }

    public static void SetDoubleClickCommand(DependencyObject obj, ICommand value)
    {
        obj.SetValue(DoubleClickCommandProperty, value);
    }

    public static object GetDoubleClickCommandParameter(DependencyObject obj)
    {
        return obj.GetValue(DoubleClickCommandParameterProperty);
    }

    public static void SetDoubleClickCommandParameter(DependencyObject obj, object value)
    {
        obj.SetValue(DoubleClickCommandParameterProperty, value);
    }

    // Using a DependencyProperty as the backing store for DoubleClickCommand.  This enables animation, styling, binding, etc... 
    public static readonly DependencyProperty DoubleClickCommandProperty =
        DependencyProperty.RegisterAttached("DoubleClickCommand", typeof(ICommand), typeof(Attached), new UIPropertyMetadata(null, CommandChanged));
    public static readonly DependencyProperty DoubleClickCommandParameterProperty =
        DependencyProperty.RegisterAttached("DoubleClickCommandParameter", typeof(object), typeof(Attached));

    static void CommandChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        var fe = obj as FrameworkElement;

        if (e.OldValue == null && e.NewValue != null)
        {
            fe.AddHandler(Button.MouseDoubleClickEvent, ExecuteCommand);
        }
        else if (e.OldValue != null && e.NewValue == null)
        {
            fe.RemoveHandler(Button.MouseDoubleClickEvent, ExecuteCommand);
        }
    }

    static void ExecuteCommand(object sender, RoutedEventArgs e)
    {
        var ele = sender as Button;
        ICommand command = GetDoubleClickCommand(ele);
        object parameter = GetDoubleClickCommandParameter(ele);

        command.Execute(parameter);
    }
}

Caveat: the above is just typed in the web browser. Because of the lack of a good Minimal, Complete, and Verifiable code example, I did not bother to try to compile, never mind run, the above. I trust that if there are typographical or logic errors, they are minimal and you'll be able to easily understand what the code actually ought to be, based on your goals.

The main thing here is that I've added the Attached.DoubleClickCommandParameter attached property. This will allow you to set the command parameter at the same time as the command itself.

I also changed a couple of other implementation details:

  1. The command and its parameter are retrieved for the given object when the event is raised, instead of saving the ICommand in a static field as your implementation had it. The way your code had it, you could only ever have one command at a time. If you tried to set the attached property on multiple elements, and used more than one ICommand value, you still would only ever get the most recently-set ICommand. With my change, you'll always get the command you set.
  2. I changed the code dealing with changes in the property, so that it only ever adds the handler if the previous value was null and the new value is non-null, and I also changed the code to remove the handler if and when the value is ever changed from a non-null value back to null.

Then you can use the attached properties in code-behind like this:

Attached.SetDoubleClickCommand(btn, ((CardManagementViewModel)values[1]).ChangeImageCommand);
Attached.SetDoubleClickCommandParameter(btn, ((CardManagementViewModel)values[1]).ChangeImageCommandParameter);

Note that I'm assuming you have a ChangeImageCommandParameter property that stores the parameter you want to send. You can of course set the property value to whatever you want, such as e.g. a value referring to the selected item or something else.

I also changed the setting to call the Attached class's property setter methods, which is a more proper use of the attached property abstraction in WPF. Granted, in most implementations it's exactly the same as calling the SetValue() method directly, but it is better to go through the attached property's methods, in case they have customized the behavior in some way.


Now, all that said, I will reiterate that your broader design is very wrong in several different ways. By ignoring the conventional approach of MVVM or similar, tying UI configuration and behaviors to the view models, and especially of using the converter as a place to actually modify the state of the objects, you are creating a system that is likely to have a number of subtle, difficult-to-find, and nearly-impossible-to-fix bugs in it.

But that's mostly independent of the question of how to use the attached property. Even in a well-designed WPF program, attached properties have their place, and I hope that the above gives you a better idea of how you would extend your existing attached property so that it supports additional values (e.g. the CommandProperty value).

Upvotes: 2

Related Questions