在WPF应用中使用MVVM/MVVMLight时如何与UI元素交互

4

根据下面的代码,我想要能够在点击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;
        });
    }
}

1
只需将事件处理程序放在代码后面,然后在那里更改颜色。按钮及其背景是完全的视图概念-您不必以任何方式涉及您的模型来处理此类事情。现在,如果这些不仅仅是颜色,而是例如表示按钮状态(例如,“繁忙”或“禁用”颜色) - 那就是另一回事了。但是,仅仅为了单击时更改按钮颜色而使用视图模型(甚至像一个答案建议的信使一样)是完全多余且没有太多意义。 - Evk
@Evk 颜色将根据在viewModel中发生的某些条件而来回更改。 - fs_tigre
3个回答

4

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

在View中
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中引入任何视图特定的东西),并且我需要更精细的控制来操作控件时,我会使用这些方法。


2
使用中介者模式(Messenger)是一个好建议。它可以很好地解耦。回答得好。 - Nkosi
FYI - Messenger声明缺少一个闭括号,应该像这样... Messenger.Default.Register<ChangeBackgroundMessage>(this, m => ReceiveChangeBackgroundMessage(m)); - fs_tigre
@ flyte - 你能否详细解释一下你在第一个例子中的评论“如果您需要确保此操作仅在UI线程上执行,请使用DispatcherHelper类”?如果我不使用它,我会得到错误“System.InvalidOperationException”的异常,在GalaSoft.MvvmLight.Platform.dll中发生,但未在用户代码中处理。删除它可以正常工作button2.Background = m.TheColor - fs_tigre
1
因此,异常的原因很可能是DispatcherHelper未初始化:必须在调用任何DispatcherHelper.CheckBeginInvokeOnUI之前调用DispatcherHelper.Initialize();。现在它确保在提供的函数中执行的任何代码都在UI线程上执行。例如,如果另一个线程发送了消息,并且您想修改UI控件,则必须在UI线程上执行。因此,上面的代码将确保发生这种情况。但是,由于在您的情况下,ButtonCommand的执行确实发生在UI线程上,因此没有问题。 - flyte
有意义,非常感谢! - fs_tigre

3
有两种方法可以解决这个问题 - 第一种是将Button2的背景颜色简单地绑定到视图模型上的属性。 你可以将其从视图模型中公开为brush; 尽管与MVVM更一致的方式是创建一个值转换器。
这个想法是,虽然Button2的背景与Button1相关联,但实际上它与在Button1被按下时已改变的状态相关联; 然后,值转换器将状态(这是视图模型的领域)与颜色(视图的领域)进行映射。
以这种方式处理意味着您可以在button1的视图模型命令中更改状态,而无需涉及button1_click事件,因为它现在是不必要的。
这个问题说明了你可能会如何实现这个功能: This question

1
我认为,在视图模型中拥有类型为Brush的属性不仅是“与MVVM不太一致”,而且完全破坏了这个概念,因为这个类是WPF(因此-视图)特定的(它在PresentationCore程序集中)。 - Evk
1
同意,这就是为什么我提供了两种方法来解决这个问题的答案。 - flyte
1
@flyte 但是你的代码有完全相同的问题:你在消息类中有一个类型为Brush的属性,而你在视图模型中使用它(在第二种方法中,你也在视图模型中使用了Brush)。我不能简单地拿到你的视图模型并在Android界面上使用它,例如(甚至无法进行单元测试)- 它依赖于WPF PresentationCore程序集。 - Evk
1
在我看来,采用哪种方法取决于 OP 想要更改按钮颜色的原因。可以说,如果这纯粹是一个视图概念,那么你可能无论如何都希望 Android 有不同的行为方式。 - Paul Michaels
1
@Evk,现在我们进入了语义学。答案展示了一种方法,而不是真正讨论MVVM解决方案的纯度。为了解决您对PresentationCore依赖的担忧,为什么不简单地使Message类包含颜色的众所周知的字符串,或使用System.Media.Colors.Color类型呢?至于在Xamarin中的可移植性,在OP中没有提到,所以我不用担心这个问题。 - flyte
1
@flyte 我认为这个问题的整个方法都是有缺陷的。如果你想在点击时改变按钮颜色 - 在代码后台中进行更改即可。但更有可能的是,点击button1会执行某些操作(顺便说一下,这些操作不应该被命名为无意义的“Button1Command”),这些操作会改变模型状态。作为对模型状态更改的反应,button2的背景应该改变。因此,背景颜色应该绑定到某个具有适当转换器的模型状态属性。ViewModel不是决定视图上哪个颜色按钮应该在单击另一个按钮时使用的正确位置。 - Evk

1

首先,在您的视图模型中声明一个属性,该属性将控制背景颜色,以及一个命令处理程序,按钮可以调用该处理程序来切换它。这可能看起来有点冗长,但在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的一个目标是尽可能地减少视图和视图模型之间的耦合。按钮的命令绑定应该相当简单,但要设置第二个按钮的背景色,您可以使用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>

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

enter image description here


1
我非常喜欢这种方法,因为它不涉及代码后台。非常感谢! - fs_tigre

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接