Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Change Background Color for WPF textbox in changed-state

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

like image 773
ollifant Avatar asked Aug 03 '09 19:08

ollifant


3 Answers

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.

like image 130
Bryan Anderson Avatar answered Nov 09 '22 13:11

Bryan Anderson


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}}"/>
like image 4
Charlie Avatar answered Nov 09 '22 12:11

Charlie


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.

like image 3
Kenan E. K. Avatar answered Nov 09 '22 14:11

Kenan E. K.