Question

I have a class EmployeeViewModel with 2 properties "FirstName" and "LastName". The class also has a dictionary with the changes of the properties. (The class implements INotifyPropertyChanged and IDataErrorInfo, everything is fine.

In my view there is a textbox:

<TextBox x:Name="firstNameTextBox" Text="{Binding Path=FirstName}" />

How can I change the background color of the textbox, if the original value changed? I thought about creating a trigger which sets the background color but to what should I bind? I don't want to created an additional property for every control which holds the state wheter the one was changed or not.

Thx

Was it helpful?

Solution

Just use a MultiBinding with the same property twice but have Mode=OneTime on one of the bindings. Like this:

Public Class MVCBackground
    Implements IMultiValueConverter

    Public Function Convert(ByVal values() As Object, ByVal targetType As System.Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IMultiValueConverter.Convert
        Static unchanged As Brush = Brushes.Blue
        Static changed As Brush = Brushes.Red

        If values.Count = 2 Then
            If values(0).Equals(values(1)) Then
                Return unchanged
            Else
                Return changed
            End If
        Else
            Return unchanged
        End If
    End Function

    Public Function ConvertBack(ByVal value As Object, ByVal targetTypes() As System.Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object() Implements System.Windows.Data.IMultiValueConverter.ConvertBack
        Throw New NotImplementedException()
    End Function
End Class

And in the xaml:

<TextBox Text="{Binding TestText}">
    <TextBox.Background>
        <MultiBinding Converter="{StaticResource BackgroundConverter}">
            <Binding Path="TestText"    />
            <Binding Path="TestText" Mode="OneTime" />
        </MultiBinding>
    </TextBox.Background>
</TextBox>

No extra properties or logic required and you could probably wrap it all into your own markup extension. Hope that helps.

OTHER TIPS

You will need to use a value converter (converting string input to color output) and the simplest solution involves adding at least one more property to your EmployeeViewModel. You need to make some sort of a Default or OriginalValue property, and compare against that. Otherwise, how will you know what the "original value" was? You cannot tell if the value changed unless there is something holding the original value to compare against.

So, bind to the text property and compare the input string to the original value on the view model. If it has changed, return your highlighted background color. If it matches, return the normal background color. You will need to use a multi-binding if you want to compare the FirstName and LastName together from a single textbox.

I have constructed an example that demonstrates how this could work:

<Window x:Class="TestWpfApplication.Window11"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestWpfApplication"
Title="Window11" Height="300" Width="300"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Window.Resources>
    <local:ChangedDefaultColorConverter x:Key="changedDefaultColorConverter"/>
</Window.Resources>
<StackPanel>
    <StackPanel Orientation="Horizontal">
        <TextBlock>Default String:</TextBlock>
        <TextBlock Text="{Binding Path=DefaultString}" Margin="5,0"/>
    </StackPanel>
    <Border BorderThickness="3" CornerRadius="3"
            BorderBrush="{Binding ElementName=textBox, Path=Text, Converter={StaticResource changedDefaultColorConverter}}">
        <TextBox Name="textBox" Text="{Binding Path=DefaultString, Mode=OneTime}"/>
    </Border>
</StackPanel>

And here is the code-behind for the Window:

/// <summary>
/// Interaction logic for Window11.xaml
/// </summary>
public partial class Window11 : Window
{
    public static string DefaultString
    {
        get { return "John Doe"; }
    }

    public Window11()
    {
        InitializeComponent();
    }
}

Finally, here is the converter you use:

public class ChangedDefaultColorConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        string text = (string)value;
        return (text == Window11.DefaultString) ?
            Brushes.Transparent :
            Brushes.Yellow;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

And even though I wrapped a border around the TextBox (because I think that looks a little better), the Background binding can be done exactly the same way:

<TextBox Name="textBox" Text="{Binding Path=DefaultString, Mode=OneTime}"
         Background="{Binding ElementName=textBox, Path=Text, Converter={StaticResource changedDefaultColorConverter}}"/>

If you're using the MVVM paradigm, you should consider the ViewModels as having the role of adapters between the Model and the View.

It is not expected of the ViewModel to be completely agnostic of the existence of a UI in every way, but to be agnostic of any specific UI.

So, the ViewModel can (and should) have the functionality of as many Converters as possible. The practical example here would be this:

Would a UI require to know if a text is equal to a default string?

If the answer is yes, it's sufficient reason to implement an IsDefaultString property on a ViewModel.

public class TextViewModel : ViewModelBase
{
    private string theText;

    public string TheText
    {
        get { return theText; }
        set
        {
            if (value != theText)
            {
                theText = value;
                OnPropertyChanged("TheText");
                OnPropertyChanged("IsTextDefault");
            }
        }
    }

    public bool IsTextDefault
    {
        get
        {
            return GetIsTextDefault(theText);
        }
    }

    private bool GetIsTextDefault(string text)
    {
        //implement here
    }
}

Then bind the TextBox like this:

<TextBox x:Name="textBox" Background="White" Text="{Binding Path=TheText, UpdateSourceTrigger=LostFocus}">
    <TextBox.Resources>
        <Style TargetType="TextBox">
            <Style.Triggers>
                <DataTrigger Binding="{Binding IsTextDefault}" Value="False">
                    <Setter Property="TextBox.Background" Value="Red"/>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </TextBox.Resources>
</TextBox>

This propagates text back to the ViewModel upon TextBox losing focus, which causes a recalculation of the IsTextDefault. If you need to do this a lot of times or for many properties, you could even cook up some base class like DefaultManagerViewModel.

You could add to your ViewModel boolean properties like IsFirstNameModified and IsLastNameModified, and use a trigger to change the background if the textbox according to these properties. Or you could bind the Background to these properties, with a converter that returns a Brush from a bool...

A complete diferent way would be to not implement INotifyPropertyChanged and instead descend from DependencyObject or UIElement

They implement the binding using DependencyProperty You may event use only one event handler and user e.Property to find the rigth textbox

I'm pretty sure the e.NewValue != e.OldValue check is redundant as the binding should not have changed. I also beleive there may be a way to implement the binding so the dependecyObject is the textbox and not your object...

Edit if you already inherit from any WPF class (like control or usercontrol) you are probably ok and you don't need to change to UIElement as most of WPF inherit from that class

Then you can have:

using System.Windows;
namespace YourNameSpace
{
class PersonViewer:UIElement
{

    //DependencyProperty FirstName
    public static readonly DependencyProperty FirstNameProperty =
        DependencyProperty.Register("FirstName", typeof (string), typeof (PersonViewer),
                                    new FrameworkPropertyMetadata("DefaultPersonName", FirstNameChangedCallback));

    public string FirstName {
        set { SetValue(FirstNameProperty, value); }
        get { return (string) GetValue(FirstNameProperty); }
    }

    private static void FirstNameChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) {

        PersonViewer owner = d as PersonViewer;
        if (owner != null) {
            if(e.NewValue != e.OldValue && e.NewValue != "DefaultPersonName" ) {

                //Set Textbox to changed state here

            }
        }

    }

    public void AcceptPersonChanges() {

        //Set Textbox to not changed here

    }

 }
}

A variation of the last answer could be to alwais be in the modified state unless the value is the default value.

 <TextBox.Resources>
    <Style TargetType="{x:Type TextBox}">

        <Style.Triggers>
            <Trigger Property="IsLoaded" Value="True">
                <Setter Property="TextBox.Background" Value="Red"/>
            </DataTrigger>
        </Style.Triggers>

        <Style.Triggers>
            <DataTrigger Binding="{Binding RelativeSource Self}, Path=Text" Value="DefaultValueHere">
                <Setter Property="TextBox.Background" Value=""/>
            </DataTrigger>
        </Style.Triggers>

    </Style>
</TextBox.Resources>

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top