2017-10-04 192 views
1

我需要通過鼠標點擊操作並通過我的WPF應用程序中的熱鍵操作。用戶的操作會影響應用程序控件的數據和外觀。WPF和MVVM:通過RelayCommand訪問控件

例如,以下應用程序將數據發送到茶機。您可以選擇茶品牌,類型(熱或冷)和可選配料:牛奶,檸檬和糖漿。

enter image description here

但從UI設計的角度並不好,但只是例子:

  • 如果單擊下拉菜單或輸入Ctrl+B的選擇選項列表就會出現。
  • 如果點擊輸入Ctrl+T上的「熱門」按鈕,按鈕變爲藍色,文本變爲「冷」。如果再次點擊或輸入Ctrl+T,則按鈕變爲橙色,並且文本再次變爲「熱」。
  • 如果點擊可選配料按鈕或輸入相應的快捷鍵,按鈕的背景和文字變爲灰色(表示「未選中」)。相同的操作會將相應的按鈕返回到活動狀態。

enter image description here

如果不使用MVVM並沒有定義快捷鍵,邏輯也比較簡單:

Tea tea = new Tea(); // Assume that default settings avalible 

private void ToggleTeaType(object sender, EventArgs e){ 

    // Change Data 
    if(tea.getType().Equals("Hot")){ 
     tea.setType("Cold"); 
    } 
    else{ 
     tea.setType("Hot"); 
    } 

    // Change Button Appearence 
    ChangeTeaTypeButtonAppearence(sender, e); 
} 


private void ChangeTeaTypeButtonAppearence(object sender, EventArgs e){ 

    Button clickedButton = sender as Button; 
    Style hotTeaButtonStyle = this.FindResource("TeaTypeButtonHot") as Style; 
    Style coldTeaButtonStyle = this.FindResource("TeaTypeButtonCold") as Style; 

    if (clickedButton.Tag.Equals("Hot")) { 
     clickedButton.Style = coldTeaButtonStyle; // includes Tag declaration 
     clickedButton.Content = "Cold"; 
    } 
    else (clickedButton.Tag.Equals("Cold")) { 
     clickedButton.Style = hotTeaButtonStyle; // includes Tag declaration 
     clickedButton.Content = "Hot"; 
    } 
} 

// similarly for ingredients toggles 

XAML:

<Button Content="Hot" 
      Tag="Hot" 
      Click="ToggleTeaType" 
      Style="{StaticResource TeaTypeButtonHot}"/> 

<Button Content="Milk" 
     Tag="True" 
     Click="ToggleMilk" 
     Style="{StaticResource IngredientButtonTrue}"/> 

<Button Content="Lemon" 
     Tag="True" 
     Click="ToggleLemon" 
     Style="{StaticResource IngredientButtonTrue}"/> 

<Button Content="Syrup" 
     Tag="True" 
     Click="ToggleSyrup" 
     Style="{StaticResource IngredientButtonTrue}"/> 

我改變了我類似WPF項目到MVVM,因爲感謝命令它很簡單分配快捷鍵:

<Window.InputBindings> 
    <KeyBinding Gesture="Ctrl+T" Command="{Binding ToggleTeaType}" /> 
</Window.InputBindings> 

但是,現在這是一個問題,如何設置控件的外觀。下面的代碼是無效

private RelayCommand toggleTeaType; 
public RelayCommand ToggleTeaType { 
    // change data by MVVM methods... 
    // change appearence: 
    ChangeTeaTypeButtonAppearence(object sender, EventArgs e); 
} 

我需要的命令中繼,因爲我可以把它綁定到兩個按鈕和快捷方式,但如何我可以訪問從RelayCommand視圖控件?

+0

爲什麼不使用ToggleButton呢?然後,您可以將IsToggled屬性綁定到您的數據並隨意使用它。 – CKII

+3

您可以將按鈕的屬性(例如「背景」)綁定到「茶」類的屬性。然後使用轉換器根據屬性的值設置背景顏色。 –

+0

通常,我建議你爲你的場景使用'ToggleButton'。然後你可以通過使用'IsChecked'屬性來使用MVVM。 – grek40

回答

2

您應該保持viewmodel清潔視圖的特定行爲。該視圖模型應該只是提供一個接口,用於所有相關設置,它可能看起來類似於以下(BaseViewModel會包含一些輔助的方法來實現INotifyPropertyChanged等):

public class TeaConfigurationViewModel : BaseViewModel 
{ 
    public TeaConfigurationViewModel() 
    { 
     _TeaNames = new string[] 
     { 
      "Lipton", 
      "Generic", 
      "Misc", 
     }; 
    } 
    private IEnumerable<string> _TeaNames; 
    public IEnumerable<string> TeaNames 
    { 
     get { return _TeaNames; } 
    } 


    private string _SelectedTea; 
    public string SelectedTea 
    { 
     get { return _SelectedTea; } 
     set { SetProperty(ref _SelectedTea, value); } 
    } 


    private bool _IsHotTea; 
    public bool IsHotTea 
    { 
     get { return _IsHotTea; } 
     set { SetProperty(ref _IsHotTea, value); } 
    } 


    private bool _WithMilk; 
    public bool WithMilk 
    { 
     get { return _WithMilk; } 
     set { SetProperty(ref _WithMilk, value); } 
    } 


    private bool _WithLemon; 
    public bool WithLemon 
    { 
     get { return _WithLemon; } 
     set { SetProperty(ref _WithLemon, value); } 
    } 


    private bool _WithSyrup; 
    public bool WithSyrup 
    { 
     get { return _WithSyrup; } 
     set { SetProperty(ref _WithSyrup, value); } 
    } 
} 

正如你看到的,則每個屬性設置,但視圖模型不關心如何屬性被分配。

因此,讓我們建立一些用戶界面。對於以下示例,通常假設xmlns:local指向您的項目命名空間。

我建議利用你的目的定製ToggleButton

public class MyToggleButton : ToggleButton 
{ 
    static MyToggleButton() 
    { 
     MyToggleButton.DefaultStyleKeyProperty.OverrideMetadata(typeof(MyToggleButton), new FrameworkPropertyMetadata(typeof(MyToggleButton))); 
    } 


    public Brush ToggledBackground 
    { 
     get { return (Brush)GetValue(ToggledBackgroundProperty); } 
     set { SetValue(ToggledBackgroundProperty, value); } 
    } 

    // Using a DependencyProperty as the backing store for ToggledBackground. This enables animation, styling, binding, etc... 
    public static readonly DependencyProperty ToggledBackgroundProperty = 
     DependencyProperty.Register("ToggledBackground", typeof(Brush), typeof(MyToggleButton), new FrameworkPropertyMetadata()); 
} 

而且在Themes/Generic.xaml

<Style TargetType="{x:Type local:MyToggleButton}" BasedOn="{StaticResource {x:Type ToggleButton}}"> 
    <Setter Property="Template"> 
     <Setter.Value> 
      <ControlTemplate TargetType="{x:Type local:MyToggleButton}"> 
       <Border x:Name="border1" BorderBrush="Gray" BorderThickness="1" Background="{TemplateBinding Background}" Padding="5"> 
        <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/> 
       </Border> 
       <ControlTemplate.Triggers> 
        <Trigger Property="IsChecked" Value="True"> 
         <Setter TargetName="border1" Property="Background" Value="{Binding ToggledBackground,RelativeSource={RelativeSource TemplatedParent}}"/> 
        </Trigger> 
       </ControlTemplate.Triggers> 
      </ControlTemplate> 
     </Setter.Value> 
    </Setter> 
</Style> 

現在,建立使用該切換按鈕的實際窗口內容。這是你想要的用戶界面只是一個粗略的草圖,只含無標籤和說明功能的控制:

<Grid x:Name="grid1"> 
    <StackPanel> 
     <StackPanel Orientation="Horizontal"> 
      <ComboBox 
       x:Name="cb1" 
       VerticalAlignment="Center" 
       IsEditable="True" 
       Margin="20" 
       MinWidth="200" 
       ItemsSource="{Binding TeaNames}" 
       SelectedItem="{Binding SelectedTea}"> 
      </ComboBox> 
      <local:MyToggleButton 
       x:Name="hotToggle" 
       IsChecked="{Binding IsHotTea}" 
       VerticalAlignment="Center" 
       Margin="20" MinWidth="60" 
       Background="AliceBlue" ToggledBackground="Orange"> 
       <local:MyToggleButton.Style> 
        <Style TargetType="{x:Type local:MyToggleButton}"> 
         <Setter Property="Content" Value="Cold"/> 
         <Style.Triggers> 
          <Trigger Property="IsChecked" Value="True"> 
           <Setter Property="Content" Value="Hot"/> 
          </Trigger> 
         </Style.Triggers> 
        </Style> 
       </local:MyToggleButton.Style> 
      </local:MyToggleButton> 
     </StackPanel> 
     <StackPanel Orientation="Horizontal"> 
      <local:MyToggleButton 
       x:Name="milkToggle" 
       Content="Milk" 
       IsChecked="{Binding WithMilk}" 
       VerticalAlignment="Center" 
       Margin="20" MinWidth="60" 
       Background="WhiteSmoke" ToggledBackground="LightGreen"/> 
      <local:MyToggleButton 
       x:Name="lemonToggle" 
       Content="Lemon" 
       IsChecked="{Binding WithLemon}" 
       VerticalAlignment="Center" 
       Margin="20" MinWidth="60" 
       Background="WhiteSmoke" ToggledBackground="LightGreen"/> 
      <local:MyToggleButton 
       x:Name="syrupToggle" 
       Content="Syrup" 
       IsChecked="{Binding WithSyrup}" 
       VerticalAlignment="Center" 
       Margin="20" MinWidth="60" 
       Background="WhiteSmoke" ToggledBackground="LightGreen"/> 
     </StackPanel> 
    </StackPanel> 
</Grid> 

通知樣式觸發改變HotCold之間按鈕的內容。

某處初始化DataContext的(如在窗口構造函數)

public MainWindow() 
{ 
    InitializeComponent(); 

    grid1.DataContext = new TeaConfigurationViewModel(); 
} 

在這一點上,你有一個全功能的用戶界面,它會使用默認的鼠標和鍵盤輸入方法的工作,但它贏得了」還支持你的快捷鍵。

因此,讓我們添加鍵盤快捷鍵,而不會破壞已經工作的用戶界面。一種方法是,創建和使用一些自定義的命令:

public static class AutomationCommands 
{ 
    public static RoutedCommand OpenList = new RoutedCommand("OpenList", typeof(AutomationCommands), new InputGestureCollection() 
    { 
     new KeyGesture(Key.B, ModifierKeys.Control) 
    }); 

    public static RoutedCommand ToggleHot = new RoutedCommand("ToggleHot", typeof(AutomationCommands), new InputGestureCollection() 
    { 
     new KeyGesture(Key.T, ModifierKeys.Control) 
    }); 

    public static RoutedCommand ToggleMilk = new RoutedCommand("ToggleMilk", typeof(AutomationCommands), new InputGestureCollection() 
    { 
     new KeyGesture(Key.M, ModifierKeys.Control) 
    }); 

    public static RoutedCommand ToggleLemon = new RoutedCommand("ToggleLemon", typeof(AutomationCommands), new InputGestureCollection() 
    { 
     new KeyGesture(Key.L, ModifierKeys.Control) 
    }); 

    public static RoutedCommand ToggleSyrup = new RoutedCommand("ToggleSyrup", typeof(AutomationCommands), new InputGestureCollection() 
    { 
     new KeyGesture(Key.S, ModifierKeys.Control) 
    }); 
} 

然後,您可以綁定到相應的操作這些命令在你的主窗口:

<Window.CommandBindings> 
    <CommandBinding Command="local:AutomationCommands.OpenList" Executed="OpenList_Executed"/> 
    <CommandBinding Command="local:AutomationCommands.ToggleHot" Executed="ToggleHot_Executed"/> 
    <CommandBinding Command="local:AutomationCommands.ToggleMilk" Executed="ToggleMilk_Executed"/> 
    <CommandBinding Command="local:AutomationCommands.ToggleLemon" Executed="ToggleLemon_Executed"/> 
    <CommandBinding Command="local:AutomationCommands.ToggleSyrup" Executed="ToggleSyrup_Executed"/> 
</Window.CommandBindings> 

和實施中的每個快捷方式相應的處理方法後面的窗口代碼:

private void OpenList_Executed(object sender, ExecutedRoutedEventArgs e) 
{ 
    FocusManager.SetFocusedElement(cb1, cb1); 
    cb1.IsDropDownOpen = true; 
} 

private void ToggleHot_Executed(object sender, ExecutedRoutedEventArgs e) 
{ 
    hotToggle.IsChecked = !hotToggle.IsChecked; 
} 

private void ToggleMilk_Executed(object sender, ExecutedRoutedEventArgs e) 
{ 
    milkToggle.IsChecked = !milkToggle.IsChecked; 
} 

private void ToggleLemon_Executed(object sender, ExecutedRoutedEventArgs e) 
{ 
    lemonToggle.IsChecked = !lemonToggle.IsChecked; 
} 

private void ToggleSyrup_Executed(object sender, ExecutedRoutedEventArgs e) 
{ 
    syrupToggle.IsChecked = !syrupToggle.IsChecked; 
} 

同樣,請記住這整個輸入綁定的是純粹的UI相關的,它只是改變顯示PROPERT的另一種方式這些變化將被轉移到具有相同綁定的視圖模型,就像用戶用鼠標點擊按鈕一樣。沒有理由將這些東西帶入視圖模型。

+1

非常感謝你的回答和你的時間。我會嘗試重構我的項目。如果我處理這個問題,請在這裏再回答(在評論中)。 –

+0

我很抱歉,必須聲明'xxx_Executed'方法在哪裏?在'AutomationCommands'類中? –

+0

@GurebuBokofu我在Window類中聲明瞭它。它取決於在哪裏聲明'CommandBindings'。既然你可能想讓你的快捷鍵在整個窗口中工作,那麼在窗口上定義命令是最有意義的。 – grek40

1

如何從RelayCommand訪問View控件?

您不應該。 MVVM(可論證)的重點在於分離關注點。 ViewModel包含的「狀態」由View(控件)呈現。 ViewModel /邏輯不應該直接調整視圖 - 因爲這會打破關注點的分離並將邏輯與渲染緊密結合。

你需要的是爲視圖呈現它如何顯示視圖模型中的狀態。

通常,這是通過綁定完成的。例如:ViewModel不是抓取文本框引用並設置字符串:myTextBox.SetText("some value"),而是視圖綁定到視圖模型中的屬性MyText

這是視圖的責任,決定如何在屏幕上顯示的東西。

這一切都很好,但如何?我建議,如果你想要做的使用樣式像你描述的這種變化,我會嘗試使用轉換使用綁定到視圖模型狀態(比如,一個枚舉屬性HotCold)轉換器:

<Button Content="Hot" 
     Tag="Hot" 
     Click="ToggleTeaType" 
     Style="{Binding TeaType, Converter={StaticResource TeaTypeButtonStyleConverter}}"/> 

請注意,我們正在使用WPF的綁定。我們看到的唯一參考模型是通過它的屬性TeaType

在靜態資源的定義,我們有轉換器:

<ResourceDictionary> 
     <Style x:Key="HotTeaStyle"/> 
     <Style x:Key="ColdTeaStyle"/> 

     <local:TeaTypeButtonStyleConverter 
      x:Key="TeaTypeButtonStyleConverter" 
      HotStateStyle="{StaticResource HotTeaStyle}" 
      ColdStateStyle="{StaticResource ColdTeaStyle}"/> 
    </ResourceDictionary> 

而且具有邏輯從TeaType枚舉轉換成風格在此:

public enum TeaType 
{ 
    Hot, Cold 
} 

class TeaTypeButtonStyleConverter : IValueConverter 
{ 
    public Style HotStateStyle { get; set; } 
    public Style ColdStateStyle { get; set; } 

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 
    { 
     TeaType teaType = (TeaType)value; 

     if (teaType == TeaType.Hot) 
     { 
      return HotStateStyle; 
     } 
     else if (teaType == TeaType.Cold) 
     { 
      return ColdStateStyle; 
     } 
     return null; 
    } 

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 
    { 
     throw new NotSupportedException(); 
    } 
} 

它可以變得更通用和可重用。

你還應該看看切換按鈕,他們在內部處理這種事情。

+0

謝謝你的解釋。我將努力實踐你的答案。 –

+0

我認爲其他答案是切換按鈕更好的解決方案。但是請記住轉換器存在,它們可以在MVVM中非常有用 – Joe

+0

好吧,明白了。感謝您的建議。 –