Exxoff
Exxoff

Reputation: 181

Binding commands to dynamically created buttons

I'm trying to create a program which would allow me to choose a program to start a different program based on different requirements. Basically I have a JSON document specifying Name, Path, Icon etc and a button is created for each entry.

I have a ButtonDef class that looks like this:

public class ButtonDef
{
    public int Id { get; set; }
    public string Caption { get; set; }
    public string Cmd { get; set; }
    public string Icon { get; set; }
}

I create an ObservableCollection<ButtonDef> called Buttons which is a public property in my ViewModel and populate is with ButtonDefs.

I have a RelayCommand property and corresponding method that will start the program.

Everything works if I explicitly create a RelayCommand for each button and call it in the Command directive in XAML but since I don't know how many buttons there will be, that is not an OK solution.

My XAML looks like this:

<ItemsControl ItemsSource="{Binding Buttons}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
                <Button Content="{Binding Caption}" Height="30" Width="50" Margin="10" Command="{Binding DoSomething}" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>

</ItemsControl>

The buttons are created fine, with the correct caption but the Command doesn't fire.

How can I make this happen?

Edit:

 public class MainViewModel : ViewModelBase
{

    public ObservableCollection<ButtonDef> Buttons { get; set; }

    public List<DataItem> Items { get; set; }

    public RelayCommand DoSomething { get; set; }






    /// <summary>
    /// Initializes a new instance of the MainViewModel class.
    /// </summary>
    public MainViewModel(IDataService dataService)
    {


        Items = new List<DataItem> { new DataItem("Item 1"), new DataItem("Item 2") };

        //Items.Add(new DataItem("Item 1"));

        if (Buttons == null) Buttons = new ObservableCollection<ButtonDef>();
        foreach (var item in Items)
        {
            Buttons.Add(new ButtonDef
            {
                Caption = item.Title,
                Cmd = "Path to exe"
        });
    }


}

Upvotes: 1

Views: 4001

Answers (2)

Exxoff
Exxoff

Reputation: 181

While the link provided by @dymanoid gave me som insights it took some more tweaking to make it work.

First define the RelayCommand in your ViewModellike this:

public RelayCommand<object> DoSomething {get; set;}

Initialize the RelayCommand property:

DoSomething = new RelayCommand<object>(parameter => ExecuteSomething(parameter));

void ExecuteSomething(object parameter)
{
  // Do your work here
}

XAML

The button(s) are declared in the following way:

   <ItemsControl ItemsSource="{Binding Buttons}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
                <Button Content="{Binding Caption}" Height="30" Width="50" Margin="10" Command="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}},Path=DataContext.DoSomething}" CommandParameter="{Binding Cmd}" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>

</ItemsControl>

where the RelativeSource and the "DataContext" part of the Pathenables access to the window's DataContext.

Two links which led to the solution:

WPF Command Parameter for button inside ItemsControl

and

Pass different commandparameters to same command using RelayCommand WPF

Upvotes: 1

Michael Puckett II
Michael Puckett II

Reputation: 6749

I don't agree with this approach but I'll answer the question.

You'll need to make a Binding object and then use BindingOperations to apply the binding. I'm providing an example in code below with comments.

Typically Commands don't require a notifiable binding yet CommandParameters often do. So I'm going to provide a quick example I put together that hopefully gives you enough information to add any binding you need, including the Command if you want to. If the Command never changes I strongly suggest just setting it; which is the same as setting a Binding with Mode = OneTime.

This example is ONLY to show how to do a binding in code; nothing else. If you have a StackPanel (or any panel) in your MainWindow named 'root' then you can copy and paste this code behind to play with it. (Add the necessary using statements also)

Here I provide a simple PersonViewModel, make a list of those persons (People), and then binding to the list adding ONLY a CommandParameterBinding for this example.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        Initialize();
    }

    public void Initialize()
    {
        var ids = 0;
        var people = new List<PersonViewModel>()
        {
            new PersonViewModel() { Id = ids++, Name = "Mathew"},
            new PersonViewModel() { Id = ids++, Name = "Mark"},
            new PersonViewModel() { Id = ids++, Name = "Luke"},
            new PersonViewModel() { Id = ids++, Name = "John"}
        };

        foreach (var person in people)
        {
            var button = new Button()
            {
                Content = person.Name,
                Command = person.UpdatePersonCommand
            };
            SetCommandParameterBinding(button, person);
            button.Click += (s, e) => MessageBox.Show(button.CommandParameter.ToString());
            root.Children.Add(button);
        }
    }

    //This is the method that answers your question
    private static BindingExpressionBase SetCommandParameterBinding(ButtonBase button, PersonViewModel person)
    {
        //This sets a binding that binds the 'Name' property in PersonViewModel
        //Leave constructor parameter emtpy to bind to the object itself i.e. new Binding() { Source = Person }; will bind to person
        var binding = new Binding(nameof(PersonViewModel.Name)) { Source = person };
        //This sets the binding to the button and button CommandParameterProperty
        var bindingExpression = BindingOperations.SetBinding(button, ButtonBase.CommandParameterProperty, binding);
        return bindingExpression;
    }
}

//This isn't a fully written ViewModel obviously.  It's just here to make this example work.  INotifyPropertyChanged is not completely implemented.  It also definitely doesn't belong in this namespace.
public class PersonViewModel : INotifyPropertyChanged
{
    public string Name { get; set; }
    public int Id { get; set; }
    public ICommand UpdatePersonCommand { get; }
    public event PropertyChangedEventHandler PropertyChanged;
}

Upvotes: 1

Related Questions