简体   繁体   中英

WPF MVVM : how to Update UI controllers based on Event

I have 2 TextBox controllers in my UserControl let's call them TextBox1 and TextBox2.

In my old code I update the TextBox1 background when the TextBox2 TextChanged event is fired. Using an event handler in the xaml.cs, and that was easy and straightforward.

    private void textBox_TextChanged(object sender, TextChangedEventArgs e) {
     // use controllers Names.
    }

However I was reading that this violates the MVVM standards. Which is basically don't add extra code in the xaml.cs!

During my search for an answer I found 2 approaches that I kinda understood :

1- Some people suggested I use PropertyChanged to fire another event. I noticed that the PropertyChanged event wont fired until the TextBox loses focus. This is not what I'm looking for. I want TextBox1 to update immediately after a user input something to TextBox2. However, I'm still not sure where to tell the code "change TextBox1 Background if TextBox TextChanged".

2- Another approach was using Behaviours which is totally new for me, I was able to fire the event TextChanged on TextBox2 immediately, but I didn't know how to access TextBox1 properties!

My question: What is the proper way to handle the requirement I'm looking for in MVVM approach?

The second approach is the way to go. In your viewmodel , add a ICommand DoOnTextChanged and dependency property BackgroundColor .

  • Bind the DoOnTextChanged command with the TextChanged event of TextBox1 using Behaviours
  • Bind the BackgroundColor property to the background of TextBox2 using converter.
  • In the Execute function of DoOnTextChanged , change the BackgroundColor property and you are done.

If you are using MVVMLight, binding to ICommand is easy. First add these two namespace xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" and xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Platform" and do the following:

<TextBox>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="TextChanged" >
            <cmd:EventToCommand Command="{Binding DoOnTextChanged}" PassEventArgsToCommand="False" >
            </cmd:EventToCommand>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</TextBox>

Update

As OP is using plain wpf/Xaml, I am updating my answer with implementation for plain wpf.

Add the following two helper class in your project:

public class ExecuteCommand : TriggerAction<DependencyObject>
{
    public static readonly DependencyProperty CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(ExecuteCommand));
    public ICommand Command
    {
        get
        {
            return GetValue(CommandProperty) as ICommand;
        }
        set
        {
            SetValue(CommandProperty, value);
        }
    }

    protected override void Invoke(object parameter)
    {
        if (Command != null)
        {
            if (Command.CanExecute(parameter))
            {
                Command.Execute(parameter);
            }
        }
    }
}

public class EventCommand : ICommand
{
    private Action<object> func;

    public EventCommand(Action<object> func)
    {
        this.func = func;
    }

    public bool CanExecute(object parameter)
    {
        //Use your logic here when required
        return true;
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        if (func != null)
        {
            func(parameter);
        }
    }
}

In your ViewModel, implement INotifyPropertyChanged and add the following ICommand and Background property.

public class MainViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public MainViewModel(IDataService dataService)
    {
        BackColor = Brushes.Aqua;
        DoOnTextChanged = new EventCommand((obj => BackColor = BackColor == Brushes.BurlyWood ? Brushes.Chartreuse : Brushes.BurlyWood));
    }

    public ICommand DoOnTextChanged { get; set; }

    private Brush backColor;
    public Brush BackColor
    {
        get
        {
            return backColor;
        }
        set
        {
            backColor = value;
            if (PropertyChanged != null)
            {
                PropertyChanged.Invoke(this, new PropertyChangedEventArgs("BackColor"));
            }
        }

    }
}

Finally, in you ViewName.xaml file, add this namespace xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" . You may need to add a reference to System.Windows.Interactivity . Then add the following to bind the button event to a command:

<TextBox>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="TextChanged" >
            <local:ExecuteCommand Command="{Binding DoOnTextChanged}"></local:ExecuteCommand>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</TextBox>

<TextBox Background="{Binding BackColor}"></TextBox>

Although it is a lot of code to accomplish something simple, it can be really helpful is certain cases. It is better to learn all the ways and use the one that fits perfectly to your need.

I would try to keep UI specific things in the xaml as far as it possible and use triggers in that case. Check following article about triggers and a converter for null values. DataTrigger where value is NOT null?

As previous mentioned by Bradly Uffner you should modify your bindung and add UpdateSourceTrigger="PropertyChanged" so the changes are immediately fired.

Well, I always prefer to follow MVVM when it's possible and it is perfectly possible in this situation:

You are thinking in terms of View (update the TextBox1 background when the TextBox2 TextChanged) instead of thinking in terms of business logic (update the TextBox1 background when something in the business layer [the model for example] happens [a property changes its values?

You should have a View with a TextBox1 and a TextBox2 and a ViewModel with some properties, for example:

/* These properties should implement PropertyChanged, I'm too lazy */
public string WhateverInputsTextBox1 { get; set; }
public string WhateverInputsTextBox2 { get; set; }
public bool WhateverMeansTextBox1HasChanged { get; set; }

Then you should set WhateverMeansTextBox1HasChanged to true when the content of WhatevernInputsTextBox1 changes (in the set of the property).

Finally you must bind the TextBox1 text to the WhateverInputsTextBox1 property, the TextBox2 text to the WhateverInputsTextBox2 property and the TextBox1 background to the WhateverMeansTextBox1HasChanged property using a converter to convert the true to a color and the false to a different color (check IValueConverter ). Remember to set the binding to UpdateSourceTrigger="PropertyChanged" where needed (this moves the data to the ViewModel when it is entered).

This way you have all the business logic of the View into the ViewModel and you can test it if you want to as the result of having all the responsibilities correctly distributed.

Another benefit is that (at least to me) is easier to understand the intention of the developer when I see that the background of a TextBox changes when " AccountNumberChanged " rather than when TextBox2 is edited.

You can do all of that logic in the View-Model. This specific example uses the AgentOctal.WpfLib NuGet package (disclaimer: I am the author of this package) for the base ViewModel class that raises PropertyChanged notifications, but you can use whatever system you want, as long as it property implements INotifyPropertyChanged .


In this example, the more letters you put in the first TextBox , the more blue the background of the 2nd TextBox gets.

The first TextBox has its Text property bound to the Text property on the view-model. The binding has UpdateSourceTrigger set to PropertyChanged so that the binding updates the view-model every time the property changes, not just when the control looses focus.

The 2nd TextBox has its Background property bound to a SolidColorBrush property named BackgroundColor on the view-model.

On the view-model, the setter of the TextBox contains the logic to determine to color of the 2nd TextBox .

This could probably be implemented a little better by using a Color instead of a SolidColorBrush , and an IValueConverter that can change that Color in to a Brush , but it should server as a decent starting point.

All the code lives in the view-model, the code-behind is empty.


XAML:

<Window
    x:Class="VmBindingExample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:VmBindingExample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="525"
    Height="350"
    mc:Ignorable="d">
    <Window.DataContext>
        <local:MainWindowVm />
    </Window.DataContext>
    <StackPanel Margin="20" Orientation="Vertical">
        <TextBox
            Margin="4"
            MaxLength="10"
            Text="{Binding Path=Text, UpdateSourceTrigger=PropertyChanged}" />
        <TextBox Margin="4" Background="{Binding BackgroundColor}">The color of this will reflect the length of the first textbox.</TextBox>
    </StackPanel>
</Window>

View-Model:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using AgentOctal.WpfLib;

namespace VmBindingExample
{
    using System.Windows.Media;

    public class MainWindowVm : ViewModel
    {
        private string _text;

        public string Text
        {
            get
            {
                return _text;
            }

            set
            {
                SetValue(ref _text, value);
                byte red = (byte)(255 / 10 * (10 - _text.Length));
                BackgroundColor = new SolidColorBrush(Color.FromArgb(255, red, 255, 255));
            }
        }

        private Brush _backgroundColor;

        public Brush BackgroundColor
        {
            get
            {
                return _backgroundColor;
            }

            set
            {
                SetValue(ref _backgroundColor, value);
            }
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM