4

Based on my code below, I want to be able to change the background color of a Button 2 when Button 1 is clicked.

XAML File

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

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

ViewModel File

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;
        });
    }
}
fs_tigre
  • 9,459
  • 12
  • 53
  • 111
  • 1
    Just put event handler in code behind and change color there. Buttons and their background is completely view concepts - you don't have to involve your model in any way for such thing. Now if those are not just colors but for example they represent button state (one color is "busy" or "disabled" for example) - that's different story. But using view model (and even messengers like one answer suggests) to just change button color on click is complete overkill and makes not much sense. – Evk Oct 30 '17 at 20:00
  • @Evk The color will be changed back and forward based on some condition that will happen in the viewModel. – fs_tigre Oct 31 '17 at 00:33

3 Answers3

4

In addition to what pm_2 mentioned, you could take advantage of MVVMLight's Messenger class. The VM can send a message that is received by the View to change the background.

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

And then in your VM:

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

....

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

and in your View:

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

}

Yet another alternative would be to have a "view service" that the View registers with it's ViewModel. For example:

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

In VM:

public IMySpecificViewService ViewService { get; set; } 

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

which can be called in your VM's command handler:

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

I find I use these approaches when I can't directly bind to a property in the VM, (or I don't want to bleed any View specific stuff in the VM) and I need more fine grained control over manipulating the controls.

flyte
  • 1,131
  • 6
  • 16
  • 2
    Using the mediator pattern (Messenger) was a nice suggestion. It allows for nice decoupling. Good answer. – Nkosi Oct 30 '17 at 20:08
  • FYI - The messenger declaration it's missing a closing parenthesis, it should look like this... `Messenger.Default.Register(this, m => ReceiveChangeBackgroundMessage(m));` – fs_tigre Nov 01 '17 at 15:41
  • @ flyte - Do you mind elaborating a little bit more on your comment in your first example `If you need to ensure this executes only on UI thread, use the DispatcherHelper class` If I leave it I get error `An exception of type 'System.InvalidOperationException' occurred in GalaSoft.MvvmLight.Platform.dll but was not handled in user code`. Deleting it works fine `button2.Background = m.TheColor`. – fs_tigre Nov 01 '17 at 15:47
  • 1
    So the exception is because, most likely, the DispatcherHelper wasn't initialized: `DispatcherHelper.Initialize();` has to be called before any call to `DispatcherHelper.CheckBeginInvokeOnUI` can occur. Now what it does is ensures that any code executed within the provided func is executed on the UI thread. So for example, if another thread had sent the Message, and you want to modify a UI control, it **must** happen on the UI thread. So the above code would ensure that happens. But because in your scenario, the execution of the ButtonCommand did occur on the UI thread, there isn't an issue – flyte Nov 01 '17 at 15:51
  • Make sense, Thanks a lot! – fs_tigre Nov 01 '17 at 16:05
3

There are two approaches to this that spring to mind - the first is to simply bind the background colour of Button2 to a property on the viewmodel. You could expose this from the view model as a brush; although the way that is more consistent with MVVM would be to create a value converter.

The idea being that the background of Button2, despite being linked to Button1, is actually linked to a state that has changed when Button1 is pressed; the value converter then maps the state (which is the domain of the ViewModel) with the colour (the domain of the view).

Doing is this way, means that you can change the state in the view model command of button1, but not have to involve the button1_click event, as it is now unnecessary.

This question illustrates how you might achieve this.

Paul Michaels
  • 14,477
  • 36
  • 140
  • 249
  • 1
    I'd say having property of type Brush in view model is not "less consistent with MVVM" but completely breaks the concept, because this class is WPF (and so - view) specific (its in PresentationCore assembly). – Evk Oct 30 '17 at 19:55
  • 1
    Agreed, which is why I offered my answer which address this concern with two approaches. – flyte Oct 30 '17 at 20:00
  • 1
    @flyte but your code has exactly the same problem: you have property of type Brush in your message class, which you use in viewmodel (in second approach you also use Brush in viewmodel). I cannot just take your VM and use it for android UI for example (or even unit-test) - it has a dependency on WPF PresentationCore assembly. – Evk Oct 30 '17 at 20:25
  • 1
    IMHO, which approach makes sense depends heavily on why the OP wants to change the button colour. Arguably, if it is purely a view concept then you might want different behaviour for Android anyway. – Paul Michaels Oct 30 '17 at 20:32
  • 1
    @Evk So now we're getting into semantics. The answer is showing an approach, not really to debate the purity of the MVVM solution. To address your concern about the dependency of `PresentationCore`, why not simply then have the Message class contain the well known string of the color, or use `System.Media.Colors.Color` type. As for the portability to Xamarin, it wasn't mentioned anywhere in the OP so I'm not worrying about that. – flyte Oct 30 '17 at 20:33
  • 1
    @flyte the whole approach in this question is flawed I think. If you want to change button color on click - change it in code behind. But much more likely that clicking button1 performs some action (which should not be named with meaningless 'Button1Command' by the way) which changes model state. In reaction to this model state change button2 should change background. So that background color should be bound to some model state property with appropriate convereter. ViewModel is not a right place to decide which color button on view should have on click on another button. – Evk Oct 30 '17 at 20:48
1

First of all you need to declare a property in your view model that will control the background color as well as a command handler which a button can call to toggle it. This might seem a little verbose but you soon get used to that with MVVM, and there are frameworks you can use to minimize that if it really bothers you. So here's the main view model:

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()
    {
    }

}

One of the objectives of MVVM is to have as loose coupling between the view and the view model as possible. The Button's command binding should be fairly straightforward, but to set the background of the second button you can use 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>

This will cause the second button's background to toggle between red and green as you click the first button:

enter image description here

Mark Feldman
  • 13,965
  • 2
  • 24
  • 47