将命令绑定到动态创建的按钮

Binding commands to dynamically created buttons

我正在尝试创建一个程序,它允许我根据不同的要求选择一个程序来启动不同的程序。基本上我有一个 JSON 文档指定名称、路径、图标等,并为每个条目创建一个按钮。

我有一个 ButtonDef class 看起来像这样:

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

我在我的 ViewModel 中创建了一个名为 ButtonsObservableCollection<ButtonDef>,它是一个 public 属性,并用 ButtonDef 填充。

我有一个 RelayCommand 属性 和相应的方法来启动程序。

如果我为每个按钮显式创建一个 RelayCommand 并在 XAML 的 Command 指令中调用它,一切正常,但由于我不知道会有多少个按钮,所以不是一个好的解决方案。

我的 XAML 看起来像这样:

<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>

按钮创建良好,标题正确,但命令未触发。

我怎样才能做到这一点?

编辑:

 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"
        });
    }


}

我不同意这种做法,但我会回答这个问题。

您需要创建一个 Binding 对象,然后使用 BindingOperations 来应用绑定。我在下面的代码中提供了一个带有注释的示例。

通常 Commands 不需要通知绑定,但 CommandParameters 经常需要。因此,我将提供一个我放在一起的快速示例,希望它能为您提供足够的信息来添加您需要的任何绑定,如果您愿意,可以包括 Command。如果 Command 永远不会改变,我强烈建议只设置它;这与使用 Mode = OneTime 设置 Binding 相同。

这个例子只是为了展示如何在代码中进行绑定;没有其他的。如果您的 MainWindow 中有一个名为 'root' 的 StackPanel(或任何面板),那么您可以将此代码复制并粘贴到后面以进行操作。 (还添加必要的 using 语句)

这里我提供了一个简单的PersonViewModel,列出了那些人(People),然后绑定到列表,只为这个例子添加一个CommandParameterBinding

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;
}

虽然@dymanoid 提供的 link 给了我一些见解,但需要进行更多调整才能使其发挥作用。

首先在您的 ViewModel 中定义 RelayCommand,如下所示:

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

初始化中继命令属性:

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

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

XAML

按以下方式声明按钮:

   <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>

其中 Path 的 RelativeSource 和 "DataContext" 部分允许访问 window 的 DataContext。

两个 link 导致了解决方案:

ItemsControl 中按钮的 WPF 命令参数

使用 RelayCommand WPF 将不同的命令参数传递给同一命令