Alterar a cor do fundo para WPF textbox no estado alterado
-
11-07-2019 - |
Pergunta
Eu tenho um EmployeeViewModel classe com 2 propriedades "Nome" e "Sobrenome". A classe também tem um dicionário com as mudanças das propriedades. (Os implementos classe INotifyPropertyChanged e IDataErrorInfo, está tudo bem.
Na minha opinião, não é uma caixa de texto:
<TextBox x:Name="firstNameTextBox" Text="{Binding Path=FirstName}" />
Como posso alterar a cor da caixa de texto fundo, se o valor original mudou? Pensei em criar um gatilho que define a cor de fundo, mas o que eu ligam deveria? Eu não quero criar uma propriedade adicional para cada controle que detém o estado wheter aquele foi alterado ou não.
Thx
Solução
Basta usar um MultiBinding com a mesma propriedade duas vezes, mas tem Mode = OneTime em uma das ligações. Como esta:
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
E no XAML:
<TextBox Text="{Binding TestText}">
<TextBox.Background>
<MultiBinding Converter="{StaticResource BackgroundConverter}">
<Binding Path="TestText" />
<Binding Path="TestText" Mode="OneTime" />
</MultiBinding>
</TextBox.Background>
</TextBox>
Nenhuma propriedade extra ou lógica necessária e você provavelmente poderia embrulhar tudo em sua própria extensão de marcação. Espero que ajude.
Outras dicas
Você vai precisar usar um conversor de valor (convertendo string de entrada para a saída de cores) e a solução mais simples envolve a adição de mais pelo menos uma propriedade para o seu EmployeeViewModel. Você precisa fazer algum tipo de Padrão ou OriginalValue propriedade, e comparar com isso. Caso contrário, como você vai saber o que o "valor original" era? Você não pode dizer se o valor alterado a menos que há algo mantendo o valor original para comparação.
Então, se ligam à propriedade de texto e comparar a cadeia de entrada para o valor original sobre o modelo de vista. Se tiver mudado, voltar a sua cor de fundo realçado. Se ele corresponder, retorne a cor de fundo normal. Você vai precisar usar um multi-ligação, se você quiser comparar o FirstName e LastName em conjunto a partir de uma única caixa de texto.
eu construí um exemplo que demonstra como isso poderia funcionar:
<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>
E aqui está o código-behind para a janela:
/// <summary>
/// Interaction logic for Window11.xaml
/// </summary>
public partial class Window11 : Window
{
public static string DefaultString
{
get { return "John Doe"; }
}
public Window11()
{
InitializeComponent();
}
}
Finalmente, aqui é o conversor que você usa:
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();
}
}
E mesmo que eu envolto a borda ao redor do TextBox (porque eu acho que se parece um pouco melhor), a ligação fundo pode ser feito exatamente da mesma maneira:
<TextBox Name="textBox" Text="{Binding Path=DefaultString, Mode=OneTime}"
Background="{Binding ElementName=textBox, Path=Text, Converter={StaticResource changedDefaultColorConverter}}"/>
Se você estiver usando o paradigma MVVM, você deve considerar os ViewModels como tendo o papel de adaptadores entre o modelo ea vista.
Não se espera do ViewModel para ser completamente agnóstico da existência de uma interface de usuário em todos os sentidos, mas para ser agnóstico de qualquer específica UI.
Assim, o ViewModel pode (e deve) ter a funcionalidade de tantos conversores quanto possível. O exemplo prático aqui seria esta:
que um UI precisa saber se um texto é igual a uma string padrão?
Se a resposta for Sim , é razão suficiente para implementar uma propriedade IsDefaultString
em um 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
}
}
Em seguida, ligam o TextBox
assim:
<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>
Esta propaga texto volta para o ViewModel em cima foco TextBox
perder, o que provoca um novo cálculo da IsTextDefault
. Se você precisar fazer isso um monte de vezes, ou para muitas propriedades, você pode até mesmo cozinhar alguma classe base como DefaultManagerViewModel
.
Você pode adicionar aos seus propriedades boolean ViewModel como IsFirstNameModified
e IsLastNameModified
, e usar um gatilho para mudar o fundo, se a caixa de texto de acordo com essas propriedades. Ou você pode vincular a Background
a essas propriedades, com um conversor que retorna um Brush
de um bool ...
Uma maneira diferent completa seria a de não implementar INotifyPropertyChanged e em vez descendem de DependencyObject ou UIElement
Eles implementar a ligação usando DependencyProperty Você pode usar evento apenas um manipulador de eventos e e.Property usuário a encontrar o rigth caixa de texto
Eu tenho certeza que a e.NewValue! = Verificação e.OldValue é redundante, já que a ligação não deve ter mudado. Eu também acredito que pode haver uma maneira de implementar a ligação de modo que o dependecyObject é a caixa de texto e não o seu objeto ...
Editar se você já herdar de qualquer classe WPF (como o controle ou usercontrol) provavelmente você está ok e você não precisa mudar para UIElement como a maioria de WPF herdam essa classe
Em seguida, você pode ter:
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
}
}
}
Uma variação da última resposta poderia ser a de estar sempre no estado modificado a menos que o valor é o valor padrão.
<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>