2017-10-04 193 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

好吧,明白了。感谢您的建议。 –