你收到的其他兩個答案了。不幸的是,既沒有精確解決加和刪除命令。此外,人們更喜歡主要關注代碼隱藏實現而不是XAML聲明,而且對細節來說相當稀少,而另一方更正確地集中於適當的XAML實現,但不包括正確的工作代碼和(稍微)通過引入RelayCommand
類型的額外抽象來混淆答案。
所以,我會提出自己的看法,希望這對你更有用。
雖然我同意抽象的ICommand
落實到輔助類如RelayCommand
是有用的,甚至是可取的,不幸的是這往往隱藏了什麼事情的基本機制,並要求在提供了更詳細的實施另一個答案。所以現在,讓我們忽略它。
相反,只關注需要實現的內容:接口的兩種不同實現。您的視圖模型會將這些視圖顯示爲代表要執行的命令的兩個可綁定屬性的值。
這是你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
對象(正如在項目Button
的CommandParameter
)。
產生的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
(視情況需要),是否傳遞參數等。
另一方面,每當您編寫這樣的類時,就有機會編寫一個新的錯誤。更糟的是,如果你引入了一個後來被複制/粘貼的錯誤,那麼當你最終發現錯誤時,你可能會或可能不會在它存在的任何地方修復它。
當然,反覆編寫這樣的類會變得繁瑣和耗時。
再次,這些可重複使用的抽象邏輯的「最佳實踐」的一般傳統智慧的只是具體的例子。
所以,如果我們接受了一個抽象這裏很有用(我當然有:)),接下來的問題是,是什麼抽象的樣子?有很多不同的方法來處理這個問題。引用的答案就是一個例子。這裏有一個稍微不同的方法,我已經寫了:
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(anonymous)委託中的參數,然後在調用無參數處理方法時忽略它來解決的。處理這個問題的其他方法是讓處理程序方法接受參數,然後忽略它,或者有一個非泛型類,其中處理程序委託本身可以是無參數的。恕我直言,本身並沒有「正確的方式」,根據個人喜好,可能會考慮更多或更少的各種選擇。
- 還請注意,此實現與處理
CanExecuteChanged
事件時參考答案的實現不同。在我的實現中,客戶代碼對該事件進行了細粒度的控制,代價是客戶代碼保留對該問題的DelegateCommand<T>
對象的引用,並在適當的時候調用其RaiseCanExecuteChanged()
方法。在其他實現中,它取決於CommandManager.RequerySuggested
事件。這是一種方便性與效率之間的平衡,並且在某些情況下是正確的。也就是說,客戶端代碼不得不保留對可能改變可執行狀態的命令的引用,但是如果其他路由轉移到另一個路由上,至少可能導致比事件更多的事件,在某些情況下,甚至有可能它應該被提高(這比可能的低效率差得多),可能會導致而不是。
在最後一點上,另一種方法是將ICommand
實現作爲依賴對象,並提供用於控制命令的可執行狀態的依賴項屬性。這要複雜得多,但總體上可以認爲是優越的解決方案,因爲它允許對事件的提升進行細粒度的控制,同時提供了綁定命令的可執行狀態的良好慣用方式,例如,在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
事件。恕我直言,不幸的是,在它的設計方式和WPF如何正常工作(即具有可綁定屬性)之間的界面上存在這種差異。有點奇怪,我們基本上有一個屬性,但通過名爲CanExecute()
的顯式獲取方法公開。另一方面,這種差異具有一些有用的用途,包括明確和方便地使用CommandParameter
來執行命令和檢查可執行性。這些都是有價值的目標。我只是不確定我是否做出了同樣的選擇來平衡它們與WPF中通常狀態連接的一致性(即通過綁定)。幸運的是,如果真的需要的話,以一種可綁定的方式實現接口(即如上所述)就足夠簡單了。
你釘了@Peter Duniho非常感謝你解釋解決方案的故障。非常感謝。你是否願意解釋將ICommand分解成一個幫助類有什麼好處。你可能可以展示一下如何用這個項目來做這件事嗎? – JokerMartini
對顯示ICommand助手類感興趣嗎?我很喜歡看到這樣的一個例子。 – JokerMartini
@JokerMartini:是的,我很高興。我目前沒有時間。同時,您可能需要查看回答者d.moncada引用的問答:[爲什麼使用RelayCommand](https://stackoverflow.com/a/22286816)。我沒有發現那裏的討論儘可能有幫助,但它有一個完整的代碼示例(即'RelayCommand'class)。你可以從頭開始研究,看看它如何適合我替代上面提供的兩個特殊用途的'ICommand'實現。 –