【问题标题】:How to interact with UI elements when using MVVM/MVVMLight in a WPF app在 WPF 应用程序中使用 MVVM/MVVMLight 时如何与 UI 元素交互
【发布时间】:2017-10-30 19:21:04
【问题描述】:

根据我下面的代码,我希望能够在单击Button 1 时更改Button 2 的背景颜色。

XAML 文件

    <Grid>
        <Button x:Name="Button1" 
                Content="Button 1" 
                Command="{Binding Button1Command}"/>

        <Button x:Name="Button2" 
                Content="Button 2"/>
    </Grid>

视图模型文件

public class MyViewModel : ViewModelBase
{
    public ICommand Button1Command{get;private set;}

    public MyViewModel(){
        Button1Command = new RelayCommand(() => button1_Click());
    }

    private void button1_Click()
    {
        Console.WriteLine("Button 1 clicked");

        // how can I change the background color of Button 2 here
        this.Dispatcher.Invoke(() => {
           Button2.Background = Brushes.Red;
        });
    }
}

【问题讨论】:

  • 只需将事件处理程序放在代码后面并在那里更改颜色。按钮和它们的背景完全是视图概念——你不必以任何方式让你的模型参与这样的事情。现在,如果这些不仅是颜色,而且例如它们代表按钮状态(例如,一种颜色是“忙碌”或“禁用”)-那就不同了。但是使用视图模型(甚至像一个答案所暗示的那样使用信使)只是在点击时更改按钮颜色完全是矫枉过正,没有多大意义。
  • @Evk 颜色将根据 viewModel 中发生的某些情况来回更改。

标签: c# wpf mvvm mvvm-light


【解决方案1】:

除了 pm_2 提到的内容之外,您还可以利用 MVVMLight 的 Messenger 类。 VM 可以发送 View 接收到的消息以更改背景。

public class ChangeBackgroundMessage
{
    public Brush TheColor { get; set; } 
} 

然后在你的虚拟机中:

Button1Command = new RelayCommand(() => ExecuteButtonCommand());

....

private void ExecuteButtonCommand()
{
    Messenger.Default.Send<ChangeBackgroundMessage>(new ChangeBackgroundMessage { TheColor = Brushes.Red } );
} 

在你的视野中:

public partial class MyView : UserControl
{
    public MyView()
    {
         InitializeComponent();
         Messenger.Default.Register<ChangeBackgroundMessage>(this, m => ReceiveChangeBackgroundMessage(m);
    } 

    private void ReceiveChangeBackgroundMessage(ChangeBackgroundMessage m)
    {
          // If you need to ensure this executes only on UI thread, use the
          // DispatcherHelper class

          DispatcherHelper.CheckBeginInvokeOnUI(() => button2.Background = m.TheColor);
    }

}

另一种选择是拥有一个“视图服务”,视图向它的视图模型注册。例如:

public interface IMySpecificViewService
{ 
    void ChangeButtonColor(Brush color);
} 

在虚拟机中:

public IMySpecificViewService ViewService { get; set; } 

在视图中

public partial class MyView : UserControl, IMySpecificViewService
...
public MyView()
{ 
    var vm = (MyViewModel)this.DataContext;
    vm.ViewService = (IMySpecificViewService)this;
} 

public void ChangeButtonColor(Brush color)
{
    Button2.Background = color;
}  

可以在你的虚拟机的命令处理程序中调用:

private void ExecuteButtonCommand()
{
    ViewService?.ChangeButtonColor(Brushes.Red);
} 

我发现当我无法直接绑定到 VM 中的属性时,我会使用这些方法(或者我不想泄露 VM 中的任何 View 特定内容)并且我需要更精细地控制操作控制。

【讨论】:

  • 使用中介模式(Messenger)是一个不错的建议。它允许很好的解耦。很好的答案。
  • 仅供参考 - 信使声明它缺少右括号,它应该看起来像这样...Messenger.Default.Register&lt;ChangeBackgroundMessage&gt;(this, m =&gt; ReceiveChangeBackgroundMessage(m));
  • @flyte - 你介意在你的第一个例子中详细说明你的评论If you need to ensure this executes only on UI thread, use the DispatcherHelper class 如果我离开它,我会收到错误An exception of type 'System.InvalidOperationException' occurred in GalaSoft.MvvmLight.Platform.dll but was not handled in user code。删除它工作正常button2.Background = m.TheColor
  • 所以异常是因为,很可能,DispatcherHelper 没有初始化:必须先调用DispatcherHelper.Initialize();,然后才能调用DispatcherHelper.CheckBeginInvokeOnUI。现在它所做的是确保在提供的 func 中执行的任何代码都在 UI 线程上执行。因此,例如,如果另一个线程发送了消息,并且您想要修改 UI 控件,则它必须发生在 UI 线程上。所以上面的代码将确保发生这种情况。但是因为在您的场景中,ButtonCommand 的执行确实发生在 UI 线程上,所以没有问题
  • 有道理,非常感谢!
【解决方案2】:

我想到了两种方法 - 第一种是将 Button2 的背景颜色简单地绑定到视图模型上的属性。您可以将其作为画笔从视图模型中公开;虽然更符合 MVVM 的方式是创建一个值转换器。

这个想法是 Button2 的背景,尽管与 Button1 相关联,但实际上与按下 Button1 时已更改的状态相关联;然后,值转换器将状态(即 ViewModel 的域)映射到颜色(视图的域)。

这样做,意味着你可以在button1的视图模型命令中改变状态,但不必涉及button1_click事件,因为现在没有必要了。

This question 说明了如何实现这一目标。

【讨论】:

  • 我想说,在视图模型中具有 Brush 类型的属性并不是“与 MVVM 不太一致”,而是完全打破了这个概念,因为这个类是 WPF(等等 - 视图)特定的(它在 PresentationCore大会)。
  • 同意,这就是为什么我提供了我的答案,通过两种方法解决了这个问题。
  • @flyte 但您的代码有完全相同的问题:您的消息类中有 Brush 类型的属性,您可以在 viewmodel 中使用它(在第二种方法中,您也可以在 viewmodel 中使用 Brush)。例如,我不能将您的 VM 用于 android UI(甚至是单元测试)——它依赖于 WPF PresentationCore 程序集。
  • 恕我直言,哪种方法有意义在很大程度上取决于 OP 为什么要更改按钮颜色。可以说,如果它纯粹是一个视图概念,那么无论如何您可能希望 Android 有不同的行为。
  • @Evk 所以现在我们进入语义。答案是展示一种方法,而不是真正讨论 MVVM 解决方案的纯度。为了解决您对PresentationCore 依赖性的担忧,为什么不简单地让Message 类包含众所周知的颜色字符串,或者使用System.Media.Colors.Color 类型。至于 Xamarin 的可移植性,OP 中没有提到任何地方,所以我不担心。
【解决方案3】:

首先,您需要在视图模型中声明一个属性,该属性将控制背景颜色以及一个按钮可以调用以切换它的命令处理程序。这可能看起来有点冗长,但你很快就会习惯使用 MVVM,如果它真的困扰你,你可以使用一些框架来最小化它。所以这里是主视图模型:

public class MainViewModel : ViewModelBase
{
    #region Background Color Flag

    private bool _Flag;
    public bool Flag
    {
        get { return this._Flag; }
        set
        {
            if (this._Flag != value)
            {
                this._Flag = value;
                RaisePropertyChanged(() => this.Flag);
            }
        }
    }

    #endregion Background Color Flag

    #region Button Command Handler

    private ICommand _ButtonCommand;
    public ICommand ButtonCommand
    {
        get { return this._ButtonCommand = (this._ButtonCommand ?? new RelayCommand(OnButtonPressed)); }
    }

    private void OnButtonPressed()
    {
        this.Flag = !this.Flag;
    }

    #endregion Button Command Handler

    public MainViewModel()
    {
    }

}

MVVM 的目标之一是在视图和视图模型之间实现尽可能松散的耦合。 Button 的命令绑定应该相当简单,但要设置第二个按钮的背景,您可以使用 DataTriggers:

<StackPanel Orientation="Vertical">

    <Button Content="Toggle Background" HorizontalAlignment="Left" VerticalAlignment="Top"
        Command="{Binding ButtonCommand}" />

    <Button Content="Hello World!" HorizontalAlignment="Left" VerticalAlignment="Top">
        <Button.Style>
            <Style TargetType="{x:Type Button}" BasedOn="{StaticResource {x:Type Button}}">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Flag}" Value="False">
                        <Setter Property="Background" Value="Red" />
                    </DataTrigger>
                    <DataTrigger Binding="{Binding Flag}" Value="True">
                        <Setter Property="Background" Value="Green" />
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </Button.Style>
    </Button>

</StackPanel>

当您单击第一个按钮时,这将导致第二个按钮的背景在红色和绿色之间切换:

【讨论】:

  • 我真的很喜欢这种方法,因为它不涉及代码隐藏。非常感谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-22
  • 1970-01-01
  • 2011-08-27
  • 2023-02-24
  • 1970-01-01
相关资源
最近更新 更多