更改WPF文本框在更改状态下的背景颜色

7
我是一名有用的助手,可以为您翻译文本。
我有一个名为EmployeeViewModel的类,其中包含两个属性“FirstName”和“LastName”。该类还具有更改属性的字典。(该类实现了INotifyPropertyChanged和IDataErrorInfo,一切都很好。)
在我的视图中有一个文本框:
<TextBox x:Name="firstNameTextBox" Text="{Binding Path=FirstName}" />

如果原始值更改了,我该如何更改文本框的背景颜色?我考虑创建一个触发器来设置背景颜色,但是应该绑定到什么呢? 我不想为每个控件创建一个额外的属性来保存状态是否已更改。谢谢。

1
WPF中非常常见的需求……让我始终感到惊讶的是,框架没有为此提供一个简单的事件,也许可以在绑定类上实现。 - Jack Ukleja
6个回答

12

只需使用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>

无需额外属性或逻辑,您可能可以将其全部包装到自己的标记扩展中。希望这有所帮助。


4
你需要使用一个值转换器(将字符串输入转换为颜色输出),而最简单的解决方案是向EmployeeViewModel添加至少一个属性。你需要创建一些形式的默认值原始值属性,并进行比较。否则,你怎么知道“原始值”是什么呢?除非有东西保存原始值以便进行比较,否则你无法确定值是否已更改。

因此,绑定到文本属性并将输入字符串与视图模型上的原始值进行比较。如果已更改,则返回突出显示的背景颜色。如果匹配,则返回正常的背景颜色。如果要从单个文本框中一起比较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支持? - ollifant
就像我说的那样,将背景绑定到文本属性。 - Charlie
我为您制作了一个示例,以展示如何完成绑定。 - Charlie
谢谢提供的示例,我现在理解了这个概念。我剩下的问题是如何处理ValueConverter和视图模型之间的通信。毕竟默认文本不是静态的,而是在修改之前是一个领域逻辑的值。在ValueConverter内部访问视图模型对我来说有点不太好。 - ollifant
啊哈,你碰到了使用M-V-VM和ValueConverters绑定时的主要问题之一。一个解决方案是将默认字符串作为ConverterParameter传递。这将避免检查ViewModel上的属性,但缺点是必须设置该ConverterParameter。不过,老实说,对于你提到的设计问题,没有银弹。可以用很多不同的方式来解决它。 - Charlie
自从我发布了这个问题,我已经想出了解决方案:在Background="{Name=BackColor}"中,通过绑定类使属性BackColor检查值,但是按钮的外观现在变得平淡了。我本来希望这种方法会更好。 - Chuck Savage

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


1

一种完全不同的方式是不实现 INotifyPropertyChanged,而是从 DependencyObject 或 UIElement 中继承。

它们使用 DependencyProperty 实现绑定,您甚至可以只使用一个事件处理程序并使用 e.Property 找到正确的文本框。

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

如果您已经从任何 WPF 类(如 Control 或 UserControl)继承,则可能已经很好,无需更改为 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

    }

 }
}

1
你可以在你的ViewModel中添加布尔属性,例如IsFirstNameModifiedIsLastNameModified,并使用触发器根据这些属性更改文本框的背景。或者你可以将Background绑定到这些属性上,并使用转换器从布尔值返回一个Brush...

这个解决方案不太优化,因为它涉及到添加两倍的属性(你需要跟踪IsNameModified属性和原始值,以便实际确定名称是否被修改)。添加触发器也比必要的工作更多。我会直接绑定到文本并使用转换器。 - Charlie

0
一个与上一个答案的变化是,除非值是默认值,否则始终处于修改状态。

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


所有这些都是为了更改背景属性吗? javascript - this.style.backgroundColor = "yellow"; 肯定有更简单的方法。 - Alex

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接