【问题标题】:Change Background Color for WPF textbox in changed-state在更改状态下更改 WPF 文本框的背景颜色
【发布时间】:2009-08-03 19:21:54
【问题描述】:

我有一个 EmployeeViewModel 类,它有 2 个属性“FirstName”和“LastName”。该类还有一个包含属性更改的字典。 (该类实现了INotifyPropertyChanged和IDataErrorInfo,一切正常。

在我看来有一个文本框:

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

如果原始值发生更改,如何更改文本框的背景颜色?我考虑过创建一个触发器来设置背景颜色,但我应该绑定什么? 我不想为每个控制状态的控件创建一个额外的属性,无论它是否被更改。

谢谢

【问题讨论】:

  • WPF 中一个非常常见的需求......它总是让我感到惊讶,框架没有为此提供简单的事件,也许在绑定类上

标签: wpf data-binding textbox background


【解决方案1】:

只需使用具有相同属性的 MultiBinding 两次,但在其中一个绑定上设置 Mode=OneTime。像这样:

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

在 xaml 中:

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

不需要额外的属性或逻辑,您可以将它们全部包装到您自己的标记扩展中。希望对您有所帮助。

【讨论】:

    【解决方案2】:

    您将需要使用value converter(将字符串输入转换为颜色输出),最简单的解决方案是向您的EmployeeViewModel 添加至少一个属性。您需要制作某种 DefaultOriginalValue 属性,并与之进行比较。否则,你怎么知道“原始值”是什么?除非有东西持有原始值可供比较,否则您无法判断该值是否已更改。

    因此,绑定到 text 属性并将输入字符串与视图模型上的原始值进行比较。如果已更改,请返回突出显示的背景颜色。如果匹配,则返回正常的背景颜色。如果您想在单个文本框中比较 FirstNameLastName,则需要使用多重绑定。

    我已经构建了一个示例来演示它是如何工作的:

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

    这里是窗口的代码隐藏:

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

    最后,这是你使用的转换器:

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

    即使我在 TextBox 周围包裹了一个边框(因为我认为这看起来更好一些),背景绑定也可以完全相同的方式完成:

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

    【讨论】:

    • 我现在应该将背景色绑定到什么?一个方法、一个字典条目、一个字段、一个属性?或者我完全错过了一些东西。我认为方便的唯一方法是字典,但 INotifyPropertyChanged 不支持?
    • 就像我说的,将背景绑定到文本属性。
    • 我为你做了一个例子,展示如何完成绑定。
    • 感谢您的示例,我现在理解了这个概念。我剩下的问题是如何处理 ValueConverter 和视图模型之间的通信。毕竟默认文本不是静态的,而是修改前的域逻辑值。访问 ValueConverter 内部的视图模型对我来说有点脏。
    • 啊哈,您遇到了在绑定中使用 M-V-VM 和 ValueConverters 的主要问题之一。一种解决方案是将默认字符串作为 ConverterParameter 传递。这将绕过必须检查 ViewModel 上的属性,但缺点是您必须设置该 ConverterParameter。不过,老实说,您提到的设计问题没有灵丹妙药。可以通过很多不同的方式来解决这个问题。
    【解决方案3】:

    如果您使用 MVVM 范式,则应将 ViewModel 视为在 Model 和 View 之间充当适配器的角色。

    不希望 ViewModel 在各个方面都完全不知道 UI 的存在,而是不知道任何特定 UI。

    因此,ViewModel 可以(并且应该)具有尽可能多的转换器的功能。这里的实际例子是这样的:

    UI 是否需要知道文本是否等于默认字符串?

    如果答案是,则有充分的理由在 ViewModel 上实现 IsDefaultString 属性。

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

    然后像这样绑定TextBox

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

    这会在 TextBox 失去焦点时将文本传播回 ViewModel,这会导致重新计算 IsTextDefault。如果您需要多次或为许多属性执行此操作,您甚至可以编写一些基类,例如 DefaultManagerViewModel

    【讨论】:

      【解决方案4】:

      您可以将IsFirstNameModifiedIsLastNameModified 等布尔属性添加到您的ViewModel,如果文本框根据这些属性使用触发器来更改背景。或者您可以将Background 绑定到这些属性,并使用从布尔值返回Brush 的转换器...

      【讨论】:

      • 此解决方案不太理想,因为它涉及添加两倍的属性(您需要跟踪 IsNameModified 属性以及原始值,以便您可以实际确定名称是否被修改) .添加触发器也比必要的工作更多。我会直接绑定到文本并使用转换器。
      【解决方案5】:

      完全不同的方法是不实现 INotifyPropertyChanged,而是从 DependencyObject 或 UIElement 下降

      他们使用 DependencyProperty 实现绑定 您可以仅使用一个事件处理程序和用户 e.Property 来查找正确的文本框

      我很确定 e.NewValue != e.OldValue 检查是多余的,因为绑定不应该改变。我也相信可能有一种方法可以实现绑定,所以dependecyObject是文本框而不是你的对象......

      如果您已经从任何 WPF 类(如控件或用户控件)继承,则进行编辑,您可能没问题,并且您不需要更改为 UIElement,因为大多数 WPF 都从该类继承

      那么你可以:

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

      【讨论】:

        【解决方案6】:

        最后一个答案的变体可能是始终处于修改状态,除非该值是默认值。

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

        【讨论】:

        • 所有这些都更改为 backgorund 属性? javascript - this.style.backgroundColor = "黄色";必须有更简单的方法。