views:

2648

answers:

6

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

+2  A: 

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}}"/>
Charlie
And to what should I now bind the background- color? A method, a dictionary entry, a field, a property?Or I am totally missing something. I think the only way it may work to be convenient would be a dictionary, but this is not supported by INotifyPropertyChanged?
ollifant
Like I said, bind the Background to the text property.
Charlie
I made an example for you, to show how the binding could be accomplished.
Charlie
Thanks for the example, I now understand the concept. The remaining problem I have is how to handle the communication between the ValueConverter and the view- model. After all the default- text is not static but instead a value of a domain logic before modifying. Accessing the view- model inside of the ValueConverter looks a bit dirty to me.
ollifant
Aha, you have hit on one of the major problems using M-V-VM and ValueConverters in bindings. One solution would be to pass the default string as the ConverterParameter. That would bypass having to check a property on the ViewModel, but the downside is that you have to set that ConverterParameter. Honestly, though, there is no silver bullet for the design problem you mention. It is something that can be addressed in a lot of different ways.
Charlie
+1  A: 

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...

Thomas Levesque
This solution is less optimal because it involves adding twice as many properties (you will need to keep track of both the IsNameModified properties as well as an original value so you can actually determine whether the name was modified). Adding triggers is also more work than necessary. I would bind directly to the text and use a converter.
Charlie
+1  A: 

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.

kek444
A: 

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>

JC
All that to change to the backgorund property? javascript - this.style.backgroundColor = "yellow"; There has to be an easier way.
Alex
A: 

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

    }

 }
}
JC
+1  A: 

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.

Bryan Anderson