将命令绑定到动态创建的按钮
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
中创建了一个名为 Buttons
的 ObservableCollection<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 导致了解决方案:
和
我正在尝试创建一个程序,它允许我根据不同的要求选择一个程序来启动不同的程序。基本上我有一个 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
中创建了一个名为 Buttons
的 ObservableCollection<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 导致了解决方案:
和