我在下面有一个非常简单的解决方案,用于基于MVVM填充标签。如何设置以下两个命令,以及“添加”和“删除”。从我在网上看到的内容看来,我需要设置ICommand或其他类似的东西。对我来说,让我开始工作对我来说还不够清楚。
添加命令将调用ViewModel类中已存在的函数。它会被一个键命令'Ctrl + N'
当用户点击“X”按钮时,将调用删除命令,这将删除该特定标签。否则可以通过“Ctrl + W”调用它,这将关闭当前选择的任何一个选项卡。
命令对我来说是新的,所以如果有人可以帮助我,我将不胜感激。我希望对此进行扩展,并继续为该工具添加更多内容。
链接到visual studio dropbox files。你会看到我已经把课程分解成课程,并以一种让事情变得清晰的方式组织起来。
以下工具的片段......
查看模型
using System;
using System.Collections.ObjectModel;
using System.Windows;
namespace WpfApplication1
{
public class ViewModel : ObservableObject
{
private ObservableCollection<TabItem> tabItems;
public ObservableCollection<TabItem> TabItems
{
get { return tabItems ?? (tabItems = new ObservableCollection<TabItem>()); }
}
public ViewModel()
{
TabItems.Add(new TabItem { Header = "One", Content = DateTime.Now.ToLongDateString() });
TabItems.Add(new TabItem { Header = "Two", Content = DateTime.Now.ToLongDateString() });
TabItems.Add(new TabItem { Header = "Three", Content = DateTime.Now.ToLongDateString() });
}
public void AddContentItem()
{
TabItems.Add(new TabItem { Header = "Three", Content = DateTime.Now.ToLongDateString() });
}
}
public class TabItem
{
public string Header { get; set; }
public string Content { get; set; }
}
}
MainWindow XAML
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:data="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="250">
<Window.DataContext>
<data:ViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!--<Button Content="Add" Command="{Binding AddCommand}" Grid.Row="0"></Button>-->
<TabControl ItemsSource="{Binding TabItems}" Grid.Row="1" Background="LightBlue">
<TabControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="{Binding Header}" VerticalAlignment="Center"/>
<Button Content="x" Width="20" Height="20" Margin="5 0 0 0"/>
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<TextBlock
Text="{Binding Content}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
答案 0 :(得分:1)
您已经收到了另外两个答案。不幸的是,它们都没有精确地解决添加和删除命令。此外,人们更倾向于主要关注代码隐藏实现而不是XAML声明,并且在细节方面相当稀疏,而另一个更正确地关注XAML中的实现(如果适用),但不包括正确的工作代码,以及(稍微)通过引入RelayCommand
类型的额外抽象来混淆答案。
所以,我会提出自己对这个问题的看法,希望这对你更有用。
虽然我同意将ICommand
实现抽象为辅助类(例如RelayCommand
)是有用的,甚至是可取的,但不幸的是,这往往隐藏了正在发生的事情的基本机制,并且需要更多在另一个答案中提供的精心实施。所以现在,让我们忽略它。
相反,只关注需要实现的内容:ICommand
接口的两种不同实现。您的视图模型将这些公开为两个可绑定属性的值,表示要执行的命令。
以下是您的ViewModel
课程的新版本(删除了不相关且未提供的ObservableObject
类型):
class ViewModel
{
private class AddCommandObject : ICommand
{
private readonly ViewModel _target;
public AddCommandObject(ViewModel target)
{
_target = target;
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
_target.AddContentItem();
}
}
private class RemoveCommandObject : ICommand
{
private readonly ViewModel _target;
public RemoveCommandObject(ViewModel target)
{
_target = target;
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
_target.RemoveContentItem((TabItem)parameter);
}
}
private ObservableCollection<TabItem> tabItems;
public ObservableCollection<TabItem> TabItems
{
get { return tabItems ?? (tabItems = new ObservableCollection<TabItem>()); }
}
public ICommand AddCommand { get { return _addCommand; } }
public ICommand RemoveCommand { get { return _removeCommand; } }
private readonly ICommand _addCommand;
private readonly ICommand _removeCommand;
public ViewModel()
{
TabItems.Add(new TabItem { Header = "One", Content = DateTime.Now.ToLongDateString() });
TabItems.Add(new TabItem { Header = "Two", Content = DateTime.Now.ToLongDateString() });
TabItems.Add(new TabItem { Header = "Three", Content = DateTime.Now.ToLongDateString() });
_addCommand = new AddCommandObject(this);
_removeCommand = new RemoveCommandObject(this);
}
public void AddContentItem()
{
TabItems.Add(new TabItem { Header = "Three", Content = DateTime.Now.ToLongDateString() });
}
public void RemoveContentItem(TabItem item)
{
TabItems.Remove(item);
}
}
请注意两个添加的嵌套类AddCommandObject
和RemoveCommandObject
。这些都是ICommand
几乎最简单的实现的例子。它们总是可以被执行,因此CanExecute()
的返回值永远不会改变(因此不需要引发CanExecuteChanged
事件)。他们确实需要引用您的ViewModel
对象,以便他们每个人都可以调用适当的方法。
还添加了两个公共属性以允许绑定这些命令。当然,RemoveContentItem()
方法需要知道要删除的项目。这需要在XAML中设置,以便可以将值作为参数传递给命令处理程序,并从那里传递给实际的RemoveContentItem()
方法。
为了支持使用键盘命令,一种方法是向窗口添加输入绑定。这就是我在这里选择的。 RemoveCommand
绑定还需要删除要作为命令参数传递的项,因此它绑定到CommandParameter
对象的KeyBinding
(就像CommandParameter
一样项目中的Button
。)
生成的XAML如下所示:
<Window.DataContext>
<data:ViewModel/>
</Window.DataContext>
<Window.InputBindings>
<KeyBinding Command="{Binding AddCommand}">
<KeyBinding.Gesture>
<KeyGesture>Ctrl+N</KeyGesture>
</KeyBinding.Gesture>
</KeyBinding>
<KeyBinding Command="{Binding RemoveCommand}"
CommandParameter="{Binding SelectedItem, ElementName=tabControl1}">
<KeyBinding.Gesture>
<KeyGesture>Ctrl+W</KeyGesture>
</KeyBinding.Gesture>
</KeyBinding>
</Window.InputBindings>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TabControl x:Name="tabControl1" ItemsSource="{Binding TabItems}" Grid.Row="1" Background="LightBlue">
<TabControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="{Binding Header}" VerticalAlignment="Center"/>
<Button Content="x" Width="20" Height="20" Margin="5 0 0 0"
Command="{Binding DataContext.RemoveCommand, RelativeSource={RelativeSource AncestorType=TabControl}}"
CommandParameter="{Binding DataContext, RelativeSource={RelativeSource Self}}">
</Button>
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<TextBlock Text="{Binding Content}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
修改强>
正如我上面提到的,实际上有利于抽象ICommand
实现,使用辅助类而不是为要实现的每个命令声明一个新类。 Why RelayCommand引用的答案提到松耦合和单元测试是动机。虽然我同意这些是好目标,但我不能说这些目标实际上是通过ICommand
实现的抽象来实现的。
相反,我认为这些优点与制作此类抽象时主要发现的相同:它允许代码重用,这样做可以提高开发人员的工作效率,以及代码的可维护性和质量。
在上面的示例中,每次需要新命令时,都必须编写一个实现ICommand
的新类。一方面,这意味着您编写的每个课程都可以根据具体目的量身定制。根据情况需要处理CanExecuteChanged
或不处理参数,等等。
另一方面,每次写这样的课程,都有机会写一个新的bug。更糟糕的是,如果你引入了一个后来被复制/粘贴的bug,那么当你最终发现bug时,你可能会或者可能不会在它存在的任何地方修复它。
当然,一遍又一遍地编写这些课程会变得乏味且耗时。
同样,这些只是&#34;最佳实践&#34;的一般传统智慧的具体例子。抽象可重用逻辑。
所以,如果我们已经接受抽象在这里很有用(我当然有:)),那么问题就变成了,抽象是什么样的?有许多不同的方法可以解决这个问题。引用的答案就是一个例子。这是我写的一种略有不同的方法:
class DelegateCommand<T> : ICommand
{
private readonly Func<T, bool> _canExecuteHandler;
private readonly Action<T> _executeHandler;
public DelegateCommand(Action<T> executeHandler)
: this(executeHandler, null) { }
public DelegateCommand(Action<T> executeHandler, Func<T, bool> canExecuteHandler)
{
_canExecuteHandler = canExecuteHandler;
_executeHandler = executeHandler;
}
public bool CanExecute(object parameter)
{
return _canExecuteHandler != null ? _canExecuteHandler((T)parameter) : true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
_executeHandler((T)parameter);
}
public void RaiseCanExecuteChanged()
{
EventHandler handler = CanExecuteChanged;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
}
在ViewModel
课程中,上面的内容将如下使用:
class ViewModel
{
private ObservableCollection<TabItem> tabItems;
public ObservableCollection<TabItem> TabItems
{
get { return tabItems ?? (tabItems = new ObservableCollection<TabItem>()); }
}
public ICommand AddCommand { get { return _addCommand; } }
public ICommand RemoveCommand { get { return _removeCommand; } }
private readonly ICommand _addCommand;
private readonly ICommand _removeCommand;
public ViewModel()
{
TabItems.Add(new TabItem { Header = "One", Content = DateTime.Now.ToLongDateString() });
TabItems.Add(new TabItem { Header = "Two", Content = DateTime.Now.ToLongDateString() });
TabItems.Add(new TabItem { Header = "Three", Content = DateTime.Now.ToLongDateString() });
// Use a lambda delegate to map the required Action<T> delegate
// to the parameterless method call for AddContentItem()
_addCommand = new DelegateCommand<object>(o => this.AddContentItem());
// In this case, the target method takes a parameter, so we can just
// use the method directly.
_removeCommand = new DelegateCommand<TabItem>(RemoveContentItem);
}
注意:
ICommand
实现。已从AddCommandObject
类中删除RemoveCommandObject
和ViewModel
类。DelegateCommand<T>
类。ICommand.Execute(object)
方法的参数。在上面,通过接受lambda(匿名)委托中的参数,然后在调用无参数处理程序方法时忽略它来解决这个问题。处理此问题的其他方法是让处理程序方法接受参数,然后忽略它,或者使用非泛型类,其中处理程序委托本身可以是无参数的。恕我直言,没有&#34;正确的方式&#34;本身...只是根据个人喜好可能被认为或多或少的各种选择。CanExecuteChanged
事件时的实现不同。在我的实现中,客户端代码被赋予对该事件的细粒度控制,代价是要求客户端代码保留对所讨论的DelegateCommand<T>
对象的引用并在其中调用其RaiseCanExecuteChanged()
方法。适当的时候。在另一个实现中,它依赖于CommandManager.RequerySuggested
事件。这是方便性与效率之间的权衡,在某些情况下是正确性。也就是说,客户端代码必须保留对可能改变可执行状态的命令的引用不太方便,但是如果一个人走另一条路径,至少可以更频繁地引发CanExecuteChanged
事件。超过要求,在某些情况下,它甚至可能不应该被提出(这比可能的低效率更糟糕)。在最后一点,另一种方法是使ICommand
实现成为依赖项对象,并提供用于控制命令可执行状态的依赖项属性。这要复杂得多,但整体可以被认为是优秀的解决方案,因为它允许对CanExecuteChanged
事件的提升进行细粒度控制,同时提供一种良好的,惯用的方式来绑定可执行状态命令,例如在XAML中,任何属性或属性实际上都决定了所述可执行性。
这样的实现可能看起来像这样:
class DelegateDependencyCommand<T> : DependencyObject, ICommand
{
public static readonly DependencyProperty IsExecutableProperty = DependencyProperty.Register(
"IsExecutable", typeof(bool), typeof(DelegateCommand<T>), new PropertyMetadata(true, OnIsExecutableChanged));
public bool IsExecutable
{
get { return (bool)GetValue(IsExecutableProperty); }
set { SetValue(IsExecutableProperty, value); }
}
private static void OnIsExecutableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DelegateDependencyCommand<T> command = (DelegateDependencyCommand<T>)d;
EventHandler handler = command.CanExecuteChanged;
if (handler != null)
{
handler(command, EventArgs.Empty);
}
}
private readonly Action<T> _executeHandler;
public DelegateDependencyCommand(Action<T> executeHandler)
{
_executeHandler = executeHandler;
}
public bool CanExecute(object parameter)
{
return IsExecutable;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
_executeHandler((T)parameter);
}
}
在上面,该类的canExecuteHandler
参数被取消,而不是IsExecutable
属性。当该属性更改时,将引发CanExecuteChanged
事件。恕我直言,遗憾的是ICommand
界面之间的差异在于它的设计方式和WPF的正常工作方式(即具有可绑定属性)。我们基本上有一个属性但通过一个名为CanExecute()
的显式getter方法公开,这有点奇怪。
另一方面,这种差异确实有一些有用的目的,包括明确和方便地使用CommandParameter
来执行命令和检查可执行性。这些都是值得的目标。我个人不确定我是否做出了相同的选择,以便在WPF中连接状态的一致方式(即通过绑定)保持平衡。幸运的是,如果真的需要,可以以可绑定的方式(如上所述)实现ICommand
接口,这很简单。
答案 1 :(得分:0)
您可以从删除开始。
首先,您需要创建一个RelayCommand
类。有关RelayCommand的更多信息,请参阅此帖子:Why RelayCommand
public class RelayCommand : ICommand
{
#region Private members
/// <summary>
/// Creates a new command that can always execute.
/// </summary>
private readonly Action execute;
/// <summary>
/// True if command is executing, false otherwise
/// </summary>
private readonly Func<bool> canExecute;
#endregion
/// <summary>
/// Initializes a new instance of <see cref="RelayCommand"/> that can always execute.
/// </summary>
/// <param name="execute">The execution logic.</param>
public RelayCommand(Action execute): this(execute, canExecute: null)
{
}
/// <summary>
/// Initializes a new instance of <see cref="RelayCommand"/>.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
public RelayCommand(Action execute, Func<bool> canExecute)
{
if (execute == null)
{
throw new ArgumentNullException("execute");
}
this.execute = execute;
this.canExecute = canExecute;
}
/// <summary>
/// Raised when RaiseCanExecuteChanged is called.
/// </summary>
public event EventHandler CanExecuteChanged;
/// <summary>
/// Determines whether this <see cref="RelayCommand"/> can execute in its current state.
/// </summary>
/// <param name="parameter">
/// Data used by the command. If the command does not require data to be passed, this object can be set to null.
/// </param>
/// <returns>True if this command can be executed; otherwise, false.</returns>
public bool CanExecute(object parameter)
{
return this.canExecute == null ? true : this.canExecute();
}
/// <summary>
/// Executes the <see cref="RelayCommand"/> on the current command target.
/// </summary>
/// <param name="parameter">
/// Data used by the command. If the command does not require data to be passed, this object can be set to null.
/// </param>
public void Execute(object parameter)
{
this.execute();
}
/// <summary>
/// Method used to raise the <see cref="CanExecuteChanged"/> event
/// to indicate that the return value of the <see cref="CanExecute"/>
/// method has changed.
/// </summary>
public void RaiseCanExecuteChanged()
{
var handler = this.CanExecuteChanged;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
}
接下来,在ViewModel
类型RelayCommand
中添加“可绑定”删除属性。这是您的TabItem
按钮将绑定到的内容,以及它将如何通知它被按下的ViewModel
。 ReplayCommand
将有一个执行方法,只要按下按钮就会调用该方法。在这里,我们从整个TabItem
列表中删除了我们自己。
public class ViewModel : ObservableObject
{
private ObservableCollection<TabItem> tabItems;
private RelayCommand<object> RemoveCommand;
public ObservableCollection<TabItem> TabItems
{
get { return tabItems ?? (tabItems = new ObservableCollection<TabItem>()); }
}
public ViewModel()
{
TabItems.Add(new TabItem { Header = "One", Content = DateTime.Now.ToLongDateString() });
TabItems.Add(new TabItem { Header = "Two", Content = DateTime.Now.ToLongDateString() });
TabItems.Add(new TabItem { Header = "Three", Content = DateTime.Now.ToLongDateString() });
RemoveCommand = new RelayCommand<object>(RemoveItemExecute);
}
public void AddContentItem()
{
TabItems.Add(new TabItem { Header = "Three", Content = DateTime.Now.ToLongDateString() });
}
private void RemoveItemExecute(object param)
{
var tabItem = param as TabItem;
if (tabItem != null)
{
TabItems.Remove(tabItem);
}
}
}
现在,更新您的XAML。每个TabItem
都需要绑定到父RemoveCommand
中的ViewModel
,并作为参数传递给自己。我们可以这样做:
<!--<Button Content="Add" Command="{Binding AddCommand}" Grid.Row="0"></Button>-->
<TabControl x:Name="TabItems" ItemsSource="{Binding TabItems}" Grid.Row="1" Background="LightBlue">
<TabControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="{Binding Header}" VerticalAlignment="Center"/>
<Button Command="{Binding ElementName=TabItems, Path=DataContext.RemoveCommand}"
CommandParameter="{Binding Path=DataContext, RelativeSource={RelativeSource Self}}"
Content="x"
Width="20"
Height="20"
Margin="5 0 0 0"/>
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<TextBlock
Text="{Binding Content}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
答案 2 :(得分:0)
首先,您需要设置命令
public static class Commands
{
private static RoutedUICommand add;
private static RoutedUICommand remove;
static Commands()
{
searchValue = new RoutedUICommand("Add", "Add", typeof(Commands));
showCSCode = new RoutedUICommand("Remove", "Remove", typeof(Commands));
add.InputGestures.Add(new KeyGesture(Key.N, ModifierKeys.Control));
remove.InputGestures.Add(new KeyGesture(Key.X));
}
public static RoutedUICommand Add { get { return add; } }
public static RoutedUICommand Remove { get { return remove; } }
}
在窗口加载的事件中,您必须绑定命令方法
<window ... Loaded="window_loaded">
cs文件
CommandBindings.Add(new CommandBinding(Commands.Remove, HandleRemoveExecuted, HandleCanRemoveExecuted));
是否启用了命令:
private void HandleCanAddExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
该命令应该做什么:
private void HandleAddExecute(object sender, ExecutedRoutedEventArgs e)
{
AddContentItem();
}
最后,您只需使用
编辑现有文件TabItems.Add(new TabItem { Header = "Three", Content = DateTime.Now.ToLongDateString(),
CommandBindings.Add(new CommandBinding(Commands.Add, HandleAddExecuted, HandleCanAddExecuted)); });
XAML:
<Window ...
xmlns:commands="clr-namespace:<NAMESPACE>">
<Button Content="x" Width="20"
Height="20" Margin="5 0 0 0"
Command="{x:Static commands:Commands.Remove}"/>