2015-10-12 87 views
1

我有一個非常簡單的解決方案,基於MVVM填充選項卡。如何設置以下兩個命令,然後選擇「添加」和「刪除」。從我在線閱讀的內容看來,我需要設置ICommand或其他東西。在演示中,我不清楚我是否能夠正常工作。wpf綁定命令到窗口的快捷方式

  1. 添加命令將調用視圖模型類的現有功能。卜它將由密鑰命令被稱爲「按Ctrl + N」

  2. 刪除當用戶點擊該「X」按鈕,這將消除特定標籤命令將被調用。否則,它可以通過'Ctrl + W'調用,這會關閉當前選中的選項卡。

命令的東西是我的新東西,所以如果有人可以幫助我,它將不勝感激。我希望能夠在此基礎上繼續增加更多的工具。

Link to visual studio dropbox files。你會看到我已經把事情分解到課堂上,並以一種清晰的方式組織它。

enter image description here

低於工具的片段......

視圖模型

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

主窗口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> 

回答

1

你收到的其他兩個答案了。不幸的是,既沒有精確解決刪除命令。此外,人們更喜歡主要關注代碼隱藏實現而不是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); 
    } 
} 

注意兩個加嵌套類,AddCommandObjectRemoveCommandObject。這些都是幾乎最簡單的ICommand實現的例子。它們可以始終執行,因此CanExecute()的返回值永不改變(所以不需要提高CanExecuteChanged事件)。他們確實需要提及您的ViewModel對象,以便他們可以分別調用適當的方法。

還有兩個公共屬性被添加來允許綁定這些命令。當然,RemoveContentItem()方法需要知道要刪除的項目。這需要在XAML中設置,以便該值可以作爲參數傳遞給命令處理程序,並從那裏傳輸到實際的方法RemoveContentItem()

爲了支持使用鍵盤的命令,一種方法是將輸入的綁定添加到窗口。這是我在這裏選擇的。所述RemoveCommand結合另外需要被刪除作爲命令參數傳遞的項目,所以這被綁定到CommandParameterKeyBinding對象(正如在項目ButtonCommandParameter)。

產生的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實現。 AddCommandObjectRemoveCommandObject類已從類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中通常狀態連接的一致性(即通過綁定)。幸運的是,如果真的需要的話,以一種可綁定的方式實現接口(即如上所述)就足夠簡單了。

+0

你釘了@Peter Duniho非常感謝你解釋解決方案的故障。非常感謝。你是否願意解釋將ICommand分解成一個幫助類有什麼好處。你可能可以展示一下如何用這個項目來做這件事嗎? – JokerMartini

+0

對顯示ICommand助手類感興趣嗎?我很喜歡看到這樣的一個例子。 – JokerMartini

+0

@JokerMartini:是的,我很高興。我目前沒有時間。同時,您可能需要查看回答者d.moncada引用的問答:[爲什麼使用RelayCommand](https://stackoverflow.com/a/22286816)。我沒有發現那裏的討論儘可能有幫助,但它有一個完整的代碼示例(即'RelayCommand 'class)。你可以從頭開始研究,看看它如何適合我替代上面提供的兩個特殊用途的'ICommand'實現。 –

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

接下來,在你的ViewModelRelayCommand類型添加一個「綁定」刪除屬性。這就是你的按鈕將綁定到什麼,以及它將如何通知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需要綁定到ViewModelRemoveCommand,並將其作爲參數傳入。我們可以做到這一點,如:

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

0

起初u需要設置你的命令

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}"/>