I am very new to WPF world so do not have much ideas about doing it. Basically,
I want to check whether value entered into textboxes are double number or not.If the value is double number then change the result textbox value to NAN and also change the color of input textbox to red.
Can anyone please guide me how to accomplish this?
Model.cs
public abstract class ObservableBase : INotifyPropertyChanged
{
public void Set<TValue>(ref TValue field, TValue newValue, [CallerMemberName] string propertyName = "")
{
if (!EqualityComparer<TValue>.Default.Equals(field, default(TValue)) && field.Equals(newValue)) return;
field = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
public abstract class ViewModelBase : ObservableBase
{
public bool IsInDesignMode
=> (bool)DesignerProperties.IsInDesignModeProperty
.GetMetadata(typeof(DependencyObject))
.DefaultValue;
}
ViewModel.cs
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
if (IsInDesignMode)
{
valueA = 2;
valueB = 3;
Calc();
}
}
#region Properties
private int valueA;
public int ValueA
{
get => valueA;
set
{
Set(ref valueA, value);
Calc();
}
}
private int valueB;
public int ValueB
{
get => valueB;
set
{
Set(ref valueB, value);
Calc();
}
}
private int valueC;
public int ValueC
{
get => valueC;
set => Set(ref valueC, value);
}
private int valueD;
public int ValueD
{
get => valueD;
set => Set(ref valueD, value);
}
#endregion
#region Methods
private void Calc()
{
ValueC = valueA + valueB;
ValueD = valueA * valueB;
}
#endregion
}
XAML
<Window x:Class="WPFTestApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPFTestApplication.ViewModel"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.Resources>
<Style TargetType="TextBlock">
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style TargetType="TextBox" x:Key="TextBox">
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Margin" Value="10"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="25"/>
<Setter Property="Grid.Column" Value="1"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Silver" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="TextBox" x:Key="TextBoxA" BasedOn="{StaticResource TextBox}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="LightBlue" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="TextBox" x:Key="TextBoxB" BasedOn="{StaticResource TextBox}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="LightGreen" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="TextBox" BasedOn="{StaticResource TextBox}"/>
</Grid.Resources>
<TextBlock Text="Value A"/>
<TextBox Text="{Binding ValueA, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource TextBoxA}"/>
<TextBlock Text="Value B" Grid.Row="1"/>
<TextBox Text="{Binding ValueB, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource TextBoxB}"
Grid.Row="1"/>
<TextBlock Text="Value C" Grid.Row="2"/>
<TextBox Text="{Binding ValueC}"
IsReadOnly="True"
Grid.Row="2"/>
<TextBlock Text="Value D" Grid.Row="3"/>
<TextBox Text="{Binding ValueD}"
IsReadOnly="True"
Grid.Row="3"/>
</Grid>
</Window>
The easiest way for your problem is to implement a ValidationRule.
Here is the code for the rule:
public class DoubleValidation : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
//You can do whatever you want here
double check;
if (!double.TryParse(value.ToString(),out check))
{
//ValidationResult(false,*) => in error
return new ValidationResult(false, "Please enter a number");
}
//ValidationResult(true,*) => is ok
return new ValidationResult(true, null);
}
}
Then in your XAML you have to refer to this ValidationRule when binding, which allows you to get the Validation.HasError property in your style.
<TextBox Validation.ErrorTemplate="{x:Null}">
<TextBox.Text>
<Binding Path="ValueB" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:DoubleValidation/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
<TextBox.Style>
<Style BasedOn="{StaticResource TextBoxB}" TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="Background" Value="Red"/>
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
Since the Error will add a red border to the TextBox, I add Validation.ErrorTemplate="{x:Null}" to keep full control.
If you want to change the Textbox value to NaN, you should do it in your ViewModel. But I would disrecommend that, since it's very boring for the user to see its inputs getting change by the UI.
I have modified your code to achieve your goal:
Model.cs
I have added ObservableBase.NotifyPropertyChanged()
public abstract class ObservableBase : INotifyPropertyChanged
{
public void Set<TValue>(ref TValue field, TValue newValue, [CallerMemberName] string propertyName = "")
{
if (!EqualityComparer<TValue>.Default.Equals(field, default(TValue)) && field.Equals(newValue)) return;
field = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public abstract class ViewModelBase : ObservableBase
{
public bool IsInDesignMode
=> (bool)DesignerProperties.IsInDesignModeProperty
.GetMetadata(typeof(DependencyObject))
.DefaultValue;
}
Then your ViewModel would look like this, you see I changed the types from int to string, then added validation flags, the trick to check whether the input is double or not is to use double.TryParse.
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
valueAisValid = true;
valueBisValid = true;
if (IsInDesignMode)
{
Calc();
}
}
#region Properties
private string valueA;
public string ValueA
{
get => valueA;
set
{
if (!string.IsNullOrEmpty(value))
{
Set(ref valueA, value);
Set(ref valueAisValid, double.TryParse(ValueA, out double d));
NotifyPropertyChanged(nameof(ValueAIsValid));
Calc();
}
}
}
private bool valueAisValid;
public bool ValueAIsValid => valueAisValid;
private string valueB;
public string ValueB
{
get => valueB;
set
{
if (!string.IsNullOrEmpty(value))
{
Set(ref valueB, value);
Set(ref valueBisValid, double.TryParse(ValueB, out double d));
NotifyPropertyChanged(nameof(ValueBIsValid));
Calc();
}
}
}
private bool valueBisValid;
public bool ValueBIsValid => valueBisValid;
private string valueC;
public string ValueC
{
get => valueC;
set => Set(ref valueC, value);
}
private string valueD;
public string ValueD
{
get => valueD;
set => Set(ref valueD, value);
}
public bool InputsValid => ValueAIsValid && ValueBIsValid;
#endregion
#region Methods
private void Calc()
{
if (InputsValid)
{
double sum = Convert.ToDouble(valueA) + Convert.ToDouble(valueB);
double product = Convert.ToDouble(valueA) * Convert.ToDouble(valueB);
ValueC = sum.ToString(CultureInfo.InvariantCulture);
ValueD = product.ToString(CultureInfo.InvariantCulture);
}
else
{
ValueC = "NAN";
ValueD = "NAN";
}
}
#endregion
}
Now here is the new guy, meet BoolToBackgroundColorConverter.
namespace WPFTestApplication
{
public class BoolToBackgroundColorConverter: IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null && !(bool)value)
{
return new SolidColorBrush(Colors.Red);
}
else if(value != null && (bool)value && parameter != null)
{
return (SolidColorBrush)parameter;
}
else
{
return new SolidColorBrush(Colors.White);
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
Now your xaml would look like:
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Window.Resources>
<local:BoolToBackgroundColorConverter x:Key="BoolToBackgroundColorConverter"/>
</Window.Resources>
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.Resources>
<SolidColorBrush x:Key="LightGreen" Color="LightGreen" />
<SolidColorBrush x:Key="LightBlue" Color="LightBlue" />
<SolidColorBrush x:Key="White" Color="White" />
<Style TargetType="TextBlock">
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style TargetType="TextBox" x:Key="TextBox">
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Margin" Value="10"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="25"/>
<Setter Property="Grid.Column" Value="1"/>
</Style>
<Style TargetType="TextBox" x:Key="TextBoxA" BasedOn="{StaticResource TextBox}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{Binding ValueAIsValid, Converter={StaticResource BoolToBackgroundColorConverter}, ConverterParameter={StaticResource LightBlue}}" />
</Trigger>
</Style.Triggers>
<Setter Property="Background" Value="{Binding ValueAIsValid, Converter={StaticResource BoolToBackgroundColorConverter}, ConverterParameter={StaticResource White}}" />
</Style>
<Style TargetType="TextBox" x:Key="TextBoxB" BasedOn="{StaticResource TextBox}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{Binding ValueBIsValid, Converter={StaticResource BoolToBackgroundColorConverter}, ConverterParameter={StaticResource LightGreen}}" />
</Trigger>
</Style.Triggers>
<Setter Property="Background" Value="{Binding ValueBIsValid, Converter={StaticResource BoolToBackgroundColorConverter}, ConverterParameter={StaticResource White}}" />
</Style>
<Style TargetType="TextBox" BasedOn="{StaticResource TextBox}"/>
</Grid.Resources>
<TextBlock Text="Value A"/>
<TextBox Text="{Binding ValueA, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource TextBoxA}"/>
<TextBlock Text="Value B" Grid.Row="1"/>
<TextBox Text="{Binding ValueB, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource TextBoxB}"
Grid.Row="1"/>
<TextBlock Text="Value C" Grid.Row="2"/>
<TextBox Text="{Binding ValueC}"
IsReadOnly="True"
Grid.Row="2"/>
<TextBlock Text="Value D" Grid.Row="3"/>
<TextBox Text="{Binding ValueD}"
IsReadOnly="True"
Grid.Row="3"/>
</Grid>
Output:
Hope this helps!
The easiest way to do this is to handle the validation in the View layer - using a control such as DoubleUpDown from the Extended WPF Toolkit, rather than trying to validate in the ViewModel by parsing a text string.
Related
This question already has answers here:
DependencyProperty not triggered
(2 answers)
What's the difference between Dependency Property SetValue() & SetCurrentValue()
(3 answers)
Closed 11 months ago.
I have a sets of user controls in my WPF application. One of them I've listed in code below:
RichSlider.xaml - control directly
<UserControl x:Name="RichSliderControl"
x:Class="CustomControls.RichSlider"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Controls;component/Themes/RichSlider.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
</UserControl>
RichSlider.xaml - control style
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:CustomControls="clr-namespace:CustomControls"
>
<Style x:Key="GlobalLabelStyle" TargetType="{x:Type FrameworkElement}">
<Setter Property="Margin" Value="0"/>
<Setter Property="TextElement.FontSize" Value="14"/>
<Setter Property="TextElement.FontWeight" Value="Bold"/>
<Setter Property="TextElement.FontFamily" Value="Century Gothic"/>
</Style>
<Style x:Key="ValueLabelStyle" TargetType="{x:Type Label}" BasedOn="{StaticResource GlobalLabelStyle}">
<Setter Property="Content" Value="{Binding Value, ElementName=RichSliderControl, Mode=OneWay}"/>
</Style>
<Style TargetType="{x:Type Slider}">
<Setter Property="Margin" Value="0"/>
<Setter Property="Foreground" Value="Gray"/>
<Setter Property="Background" Value="#F5F5F5"/>
<Setter Property="IsSnapToTickEnabled" Value="True"/>
<Setter Property="Minimum" Value="{Binding Minimum, ElementName=RichSliderControl}"/>
<Setter Property="Maximum" Value="{Binding Maximum, ElementName=RichSliderControl}"/>
<Setter Property="Value" Value="{Binding Value, ElementName=RichSliderControl}"/>
<Setter Property="SmallChange" Value="{Binding SmallChange, ElementName=RichSliderControl}"/>
<Setter Property="LargeChange" Value="{Binding LargeChange, ElementName=RichSliderControl}"/>
</Style>
<ControlTemplate x:Key="HorizontalSlider" TargetType="{x:Type UserControl}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" HorizontalAlignment="Left" VerticalContentAlignment="Center" Content="{Binding Text, ElementName=RichSliderControl, Mode=OneWay}"></Label>
<Label Grid.Row="0" Grid.Column="1" HorizontalAlignment="Right" VerticalContentAlignment="Center" Style="{StaticResource ValueLabelStyle}"></Label>
<Slider x:Name="ValueSlider"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Orientation="Horizontal"
TickPlacement="BottomRight"
>
</Slider>
</Grid>
</ControlTemplate>
<ControlTemplate x:Key="VerticalSlider" TargetType="{x:Type UserControl}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="26"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" HorizontalContentAlignment="Center" VerticalAlignment="Center" VerticalContentAlignment="Center">
<TextBlock TextWrapping="Wrap" VerticalAlignment="Center" Text="{Binding Text, ElementName=RichSliderControl, Mode=OneWay}"></TextBlock>
</Label>
<Label Grid.Row="1" Grid.Column="1" HorizontalContentAlignment="Center" Style="{StaticResource ValueLabelStyle}"></Label>
<Slider x:Name="ValueSlider"
Grid.Row="0"
Grid.Column="1"
Orientation="Vertical"
TickPlacement="TopLeft"
IsDirectionReversed="True"
>
</Slider>
</Grid>
</ControlTemplate>
<Style TargetType="{x:Type CustomControls:RichSlider}">
<Style.Triggers>
<Trigger Property="Orientation" Value="Horizontal">
<Setter Property="MinWidth" Value="100" />
<Setter Property="MinHeight" Value="20" />
<Setter Property="Template" Value="{StaticResource HorizontalSlider}" />
</Trigger>
<Trigger Property="Orientation" Value="Vertical">
<Setter Property="MinWidth" Value="20" />
<Setter Property="MinHeight" Value="100" />
<Setter Property="Template" Value="{StaticResource VerticalSlider}" />
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
RichSlider.xaml.cs - control code
public partial class RichSlider : UserControl
{
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(RichSlider));
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(double), typeof(RichSlider));
public static readonly DependencyProperty MinimumProperty =
DependencyProperty.Register("Minimum", typeof(double), typeof(RichSlider));
public static readonly DependencyProperty MaximumProperty =
DependencyProperty.Register("Maximum", typeof(double), typeof(RichSlider));
public static readonly DependencyProperty SmallChangeProperty =
DependencyProperty.Register("SmallChange", typeof(double), typeof(RichSlider));
public static readonly DependencyProperty LargeChangeProperty =
DependencyProperty.Register("LargeChange", typeof(double), typeof(RichSlider));
public static readonly DependencyProperty OrientationProperty =
DependencyProperty.Register("Orientation", typeof(Orientation), typeof(RichSlider));
private static readonly RoutedEvent ValueChangedEvent =
EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler<double>), typeof(RichSlider));
[Bindable(true)]
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
[Bindable(true)]
public double Value
{
get => (double)GetValue(ValueProperty);
set
{
var val = value;
if (val < Minimum)
{
val = Minimum;
}
if (val > Maximum)
{
val = Maximum;
}
SetValue(ValueProperty, val);
RaiseEvent(new(ValueChangedEvent));
}
}
[Bindable(true)]
public double Minimum
{
get => (double)GetValue(MinimumProperty);
set
{
var min = value;
if (Value < min)
{
Value = min;
}
if (Maximum < min)
{
Maximum = min;
}
SetValue(MinimumProperty, min);
}
}
[Bindable(true)]
public double Maximum
{
get => (double)GetValue(MaximumProperty);
set
{
var max = value;
if (Value > max)
{
Value = max;
}
if (max < Minimum)
{
max = Minimum;
}
SetValue(MaximumProperty, max);
}
}
[Bindable(true)]
public double SmallChange
{
get => (double)GetValue(SmallChangeProperty);
set
{
var change = value;
if (change > LargeChange)
{
change = LargeChange;
}
SetValue(SmallChangeProperty, change);
}
}
[Bindable(true)]
public double LargeChange
{
get => (double)GetValue(LargeChangeProperty);
set
{
var change = value;
if (change < SmallChange)
{
change = SmallChange;
}
SetValue(LargeChangeProperty, change);
}
}
[Bindable(true)]
public Orientation Orientation
{
get => (Orientation)GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
public RichSlider()
{
InitializeComponent();
}
public event RoutedPropertyChangedEventHandler<double> ValueChanged
{
add
{
AddHandler(ValueChangedEvent, value);
}
remove
{
RemoveHandler(ValueChangedEvent, value);
}
}
public override void OnApplyTemplate()
{
if (GetTemplateChild("ValueSlider") is Slider slider)
{
slider.MouseWheel += new MouseWheelEventHandler(ValueSlider_MouseWheel);
slider.ValueChanged += new RoutedPropertyChangedEventHandler<double>(ValueSlider_ValueChanged);
}
base.OnApplyTemplate();
}
private void ValueSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
RaiseEvent(new(ValueChangedEvent));
}
private void ValueSlider_MouseWheel(object sender, MouseWheelEventArgs e)
{
var directionReversed = (sender as Slider).IsDirectionReversed;
if (((e.Delta > 0 && !directionReversed) || (e.Delta < 0 && directionReversed)) && Value < Maximum)
{
Value++;
}
else if (((e.Delta < 0 && !directionReversed) || (e.Delta > 0 && directionReversed)) && Value > Minimum)
{
Value--;
}
}
}
Ok. I'm adding this control to my window.xaml and binding some value like:
xmlns:CustomControls="clr-namespace:CustomControls;assembly=Controls"
....
<CustomControls:RichSlider Minimum="1" Maximum="5" SmallChange="1" Orientation="Horizontal" Value="{Binding Path=Acceleration, Mode=OneWay}" Text="{DynamicResource Acceleration}"></CustomControls:RichSlider>
And I have a ListBox of items, which contains Name and Acceleration properties. So, when I'm clicking on ListBoxItem, the value in my RichSlider is updating according to the value of Acceleration property in item. So, seems like the binding is working ok.
But no. When I'm changing the value in my RichSlider manually, the value is no longer updated for all other items and is stuck with the value I set. As evidence:
<TextBox Text="{Binding Path=Name, Mode=OneWay}"></TextBox>
The Text value is updating after selecting another item, not depends from changing I made. I also checked the way like:
<TextBox Text="{Binding Path=Acceleration, Mode=OneWay}"></TextBox>
and it's also works fine. So, this way I made the conclusion that the issue is in my control, not in Binding, cause for TextBox it works, but for RichSlider - not.
So, does anyone have any thoughts what I'm doing wrong?
I am trying to create a banner with different states and colors to show messages to the user, showing a Border with a specific style and a Label with an style bases in the Border style and using DataTrigger. I have created each custom styles for each state in my App.xaml and I am trying to change the state based on a property of my ViewModel.
The problem is that the style doesn't change every time I change the property, but nevertheless if I modify some of the XAML during debugging, the style is refreshed correctly.
Maybe I am missing a NotifyPropertyChanged somewhere?
Visual example:
This is My code:
App.xaml with my defined styles and my custom converter
<Application.Resources>
<local:StyleConverter x:Key="StyleConverter" />
<Style x:Key="Banner" TargetType="{x:Type Border}">
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="10" />
<Setter Property="CornerRadius" Value="5" />
</Style>
<Style x:Key="Banner_Info" TargetType="{x:Type Border}" BasedOn="{StaticResource Banner}">
<Setter Property="Background" Value="#e3f7fc" />
<Setter Property="BorderBrush" Value="#8ed9f6" />
</Style>
<Style x:Key="Banner_Error" TargetType="{x:Type Border}" BasedOn="{StaticResource Banner}">
<Setter Property="Background" Value="#ffecec" />
<Setter Property="BorderBrush" Value="#f5aca6" />
</Style>
<Style x:Key="Banner_Success" TargetType="{x:Type Border}" BasedOn="{StaticResource Banner}">
<Setter Property="Background" Value="#e9ffd9" />
<Setter Property="BorderBrush" Value="#a6ca8a" />
</Style>
<Style x:Key="Banner_Text" TargetType="{x:Type Label}">
<Setter Property="FontWeight" Value="DemiBold"></Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding BorderBrush.Color, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Border}}}" Value="#8ed9f6">
<Setter Property="Foreground" Value="#31708F" />
</DataTrigger>
<DataTrigger Binding="{Binding BorderBrush.Color, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Border}}}" Value="#f5aca6">
<Setter Property="Foreground" Value="#B10009" />
</DataTrigger>
<DataTrigger Binding="{Binding BorderBrush.Color, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Border}}}" Value="#a6ca8a">
<Setter Property="Foreground" Value="#529214" />
</DataTrigger>
</Style.Triggers>
</Style>
</Application.Resources>
Styleconverter.cs
public class StyleConverter : IValueConverter {
public object Convert (object value, Type targetType, object parameter, CultureInfo culture) {
if (targetType != typeof (Style)) {
throw new InvalidOperationException ("The target must be a Style");
}
var styleProperty = parameter as string;
if (value == null || styleProperty == null) {
return null;
}
string styleValue = value.GetType ()
.GetProperty (styleProperty)
.GetValue (value, null)
.ToString ();
if (styleValue == null) {
return null;
}
Style newStyle = (Style) Application.Current.TryFindResource (styleValue);
return newStyle;
}
public object ConvertBack (object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException ();
}
}
Banner.cs
public class Banner : INotifyPropertyChanged {
private string _Message;
public string Message {
get => _Message;
private set {
_Message = value;
RaisePropertyChanged (null);
}
}
private string _Style = "Banner_Success";
public string Style {
get => _Style;
private set {
_Style = value;
RaisePropertyChanged (null);
}
}
public Banner SetSuccess (string message) {
Style = "Banner_Success";
Message = message;
return this;
}
public Banner SetInfo (string message) {
Style = "Banner_Info";
Message = message;
return this;
}
public Banner SetError (string message) {
Style = "Banner_Error";
Message = message;
return this;
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged (string PropertyName) {
PropertyChanged?.Invoke (this, new PropertyChangedEventArgs (PropertyName));
}
#endregion
}
Mainwindow.xml
<Grid>
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"></ColumnDefinition>
<ColumnDefinition Width="10*"></ColumnDefinition>
<ColumnDefinition Width="2*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Border Grid.Column="1" Grid.Row="0" VerticalAlignment="Center" Style="{Binding ., Mode=TwoWay, Converter={StaticResource StyleConverter}, ConverterParameter=BannerStyle}">
<Label Grid.Column="1" Style="{StaticResource Banner_Text}" Content="{Binding BannerMessage}" />
</Border>
</Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Padding="10" Margin="10" Content="BLUE" Command="{Binding CmdChangeColor, UpdateSourceTrigger=PropertyChanged}" CommandParameter="blue" />
<Button Padding="10" Margin="10" Content="RED" Command="{Binding CmdChangeColor, UpdateSourceTrigger=PropertyChanged}" CommandParameter="red" />
<Button Padding="10" Margin="10" Content="GREEN" Command="{Binding CmdChangeColor, UpdateSourceTrigger=PropertyChanged}" CommandParameter="green" />
</StackPanel>
</StackPanel>
</Grid>
MainViewModel.cs
public class MainViewModel : INotifyPropertyChanged {
public Banner Banner { get; } = new Banner ();
public string BannerStyle => Banner.Style;
public string BannerMessage => Banner.Message;
public RelayCommand CmdChangeColor { get; }
public MainViewModel () {
Banner.SetSuccess ("This is the initial message");
CmdChangeColor = new RelayCommand (param => ChangeColor (param.ToString ()));
}
public void ChangeColor (string color) {
switch (color) {
case "blue":
Banner.SetInfo ("INFO!! This should be a banner with blue background");
break;
case "red":
Banner.SetError ("ERROR!! This should be a banner with red background");
break;
case "green":
Banner.SetSuccess ("SUCCESS!! This should be a banner with green background");
break;
}
RaisePropertyChanged (null);
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged (string PropertyName) {
PropertyChanged?.Invoke (this, new PropertyChangedEventArgs (PropertyName));
}
#endregion
}
bind to property which has notifications, instead of entire view model ({Binding .}). This way also simplifies converter (no more reflection)
<Border Style="{Binding Path=Banner.Style, Converter={StaticResource StyleConverter}">
public class StyleConverter : IValueConverter {
public object Convert (object value, Type targetType, object parameter, CultureInfo culture) {
if (targetType != typeof (Style)) {
throw new InvalidOperationException ("The target must be a Style");
}
string styleValue = value?.ToString();
if (styleValue == null) {
return null;
}
Style newStyle = (Style) Application.Current.TryFindResource (styleValue);
return newStyle;
}
public object ConvertBack (object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException ();
}
}
I am using WPF Datagrid to group an observable collection by Parent. I have been following the example here and other examples showing a parent child relationship. So far I have the following:
And I want to get something like this:
Where the Header of a group is just a row as well but still able to collapse/expand child rows. I have tried to make a sub-datagrid without luck. So the underlying type of my collection looks something like this:
public class Task : INotifyPropertyChanged, IEditableObject
{
// memebers
...
public string ProjectName
{
get { return this.m_ProjectName; }
set
{
if (value != this.m_ProjectName)
{
this.m_ProjectName = value;
NotifyPropertyChanged("ProjectName");
}
}
}
public string TaskName
{
get { return this.m_TaskName; }
set
{
if (value != this.m_TaskName)
{
this.m_TaskName = value;
NotifyPropertyChanged("TaskName");
}
}
}
public DateTime DueDate
{
get { return this.m_DueDate; }
set
{
if (value != this.m_DueDate)
{
this.m_DueDate = value;
NotifyPropertyChanged("DueDate");
}
}
}
public bool Complete
{
get { return this.m_Complete; }
set
{
if (value != this.m_Complete)
{
this.m_Complete = value;
NotifyPropertyChanged("Complete");
}
}
}
public Task Parent
{
get { return m_Parent; }
set
{
m_Parent = value;
}
}
So my object type can either by a child or parent, where a child has a reference to its parent. So I am grouping by parent, I just haven't figured out how to make the expandable group headers rows of the same type. Any help is appreciated.
Here is the xaml:
<DataGrid x:Name="dataGrid1"
ItemsSource="{Binding Source={StaticResource cvsTasks}}"
CanUserAddRows="False"
ColumnWidth="*"
RowHeaderWidth="0">
<DataGrid.GroupStyle>
<!-- Style for groups at top level. -->
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Margin" Value="0,0,0,2"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Expander IsExpanded="True" Background="#FF112255" BorderBrush="#FF002255" Foreground="#FFEEEEEE" BorderThickness="1,1,1,5">
<Expander.Header>
<DockPanel HorizontalAlignment="Stretch" >
<TextBlock FontWeight="Bold" Text="{Binding Path=Name, Converter={StaticResource completeConverter}}" Margin="5,0,0,0" Width="200" HorizontalAlignment="Stretch"/>
<TextBlock FontWeight="Bold" Text="{Binding Path=ItemCount}" Width="Auto"/>
</DockPanel>
<!--<DataGrid x:Name="dataGrid2"
ItemsSource="{Binding Source={StaticResource vsParentTasks}}"
CanUserAddRows="False"
ColumnWidth="*"
RowHeaderWidth="0"
HeadersVisibility="None" >
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="Foreground" Value="#FFEEEEEE" />
<Setter Property="Background" Value="#FF112255" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>
</DataGrid.RowStyle>
</DataGrid>-->
</Expander.Header>
<Expander.Content>
<ItemsPresenter />
</Expander.Content>
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</DataGrid.GroupStyle>
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="Foreground" Value="Black" />
<Setter Property="Background" Value="White" />
</Style>
</DataGrid.RowStyle>
</DataGrid>
I am new to WPF and I have read a lot of similar questions on the web, but I still do not get my listview work. I want to change the background color of a list view element depending of a property (red, yellow or green)
The itemsSource of my ListView is an observable list of this class:
public class ConnectionItem
{
public ConnectionItem(string name)
{
Name = name;
}
public string Name { get; }
private string _color = "Red";
public string Color { get => _color; }
private ConnectionStatus _status;
public ConnectionStatus Status
{
set
{
if (value == _status)
{
return;
}
else
{
switch (value)
{
case ConnectionStatus.Connected:
_color = "Yellow";
break;
case ConnectionStatus.Ready:
_color = "Green";
break;
default:
_color = "Red";
break;
}
}
}
}
}
And I have defined my listview in xaml as follows:
<ListView x:Name="lvConnections">
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<EventSetter Event="MouseDoubleClick" Handler="ListViewItem_MouseDoubleClick" />
</Style>
</ListView.ItemContainerStyle>
<ListView.Resources>
<Style TargetType="ListViewItem">
<Style.Triggers>
<DataTrigger Binding="{Binding Color}" Value="Green">
<Setter Property="Background" Value="Green"/>
</DataTrigger>
<DataTrigger Binding="{Binding Color}" Value="Red">
<Setter Property="Background" Value="Red"/>
</DataTrigger>
<DataTrigger Binding="{Binding Color}" Value="Yellow">
<Setter Property="Background" Value="Yellow"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ListView.Resources>
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel Width="150" MaxHeight="50" Grid.Column="0" Grid.Row="0" Orientation="Horizontal" >
<TextBlock VerticalAlignment="Center" Text="{Binding Name}" FontWeight="ExtraBlack" TextWrapping="Wrap" Padding="10"/>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The binding does not work and all my listview element have no background color. I do not need exactly the same solution via ListView.Resources binding, but I also have did not succed in other approaches.
If you move the trigger Style from ListView.Resources to StackPanel.Resources (and change the TargetType to StackPanel) then the background colors will display using this approach.
<StackPanel Width="150" MaxHeight="50" Grid.Column="0" Grid.Row="0" Orientation="Horizontal">
<StackPanel.Resources>
<Style TargetType="StackPanel">
<Style.Triggers>
<DataTrigger Binding="{Binding Color}" Value="Green">
<Setter Property="Background" Value="Green"/>
</DataTrigger>
<DataTrigger Binding="{Binding Color}" Value="Red">
<Setter Property="Background" Value="Red"/>
</DataTrigger>
<DataTrigger Binding="{Binding Color}" Value="Yellow">
<Setter Property="Background" Value="Yellow"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Resources>
<TextBlock VerticalAlignment="Center" Text="{Binding Name}" FontWeight="ExtraBlack" TextWrapping="Wrap" Padding="10"/>
</StackPanel>
You will also need to look at implementing INotifyPropertyChanged on ConnectionItem for the colors to update when Status is changed.
public class ConnectionItem : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ConnectionItem(string name)
{
Name = name;
}
public string Name { get; }
private string _color = "Red";
public string Color
{
get => _color;
set
{
if (value == _color) return;
_color = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Color"));
}
}
private ConnectionStatus _status;
public ConnectionStatus Status
{
get => _status;
set
{
if (value == _status) return;
_status = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Status"));
switch (value)
{
case ConnectionStatus.Connected:
Color = "Yellow";
break;
case ConnectionStatus.Ready:
Color = "Green";
break;
default:
Color = "Red";
break;
}
}
}
}
Note that Status and Color now have both get and set accessors and that
that Status is setting the Color property rather than directly setting the _color field.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
lvConnections.ItemsSource = new ObservableCollection<ConnectionItem>()
{
new ConnectionItem("Starts Connected") { Status = ConnectionStatus.Connected, },
new ConnectionItem("Starts Ready") { Status = ConnectionStatus.Ready, },
new ConnectionItem("Starts Default"),
};
}
private void ListViewItem_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
var item = (sender as ListViewItem)?.DataContext as ConnectionItem;
switch (item.Status)
{
case ConnectionStatus.Connected:
item.Status = ConnectionStatus.Ready;
break;
case ConnectionStatus.Ready:
item.Status = ConnectionStatus.Disconnected;
break;
default:
item.Status = ConnectionStatus.Connected;
break;
}
}
}
You can even go one step further, remove the Color property from ConnectionItem altogether (and the switch setting it in Status) and use Status values in the Style triggers.
ConnectionItem
public class ConnectionItem : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ConnectionItem(string name)
{
Name = name;
}
public string Name { get; }
private ConnectionStatus _status;
public ConnectionStatus Status
{
get => _status;
set
{
if (value == _status) return;
_status = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Status"));
}
}
}
Style
<Style TargetType="StackPanel">
<Style.Triggers>
<DataTrigger Binding="{Binding Status}" Value="Ready">
<Setter Property="Background" Value="Green"/>
</DataTrigger>
<DataTrigger Binding="{Binding Status}" Value="Disconnected">
<Setter Property="Background" Value="Red"/>
</DataTrigger>
<DataTrigger Binding="{Binding Status}" Value="Connected">
<Setter Property="Background" Value="Yellow"/>
</DataTrigger>
</Style.Triggers>
</Style>
bind the background to the Color property.
<ListView x:Name="lvConnections">
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<EventSetter Event="MouseDoubleClick" Handler="ListViewItem_MouseDoubleClick" />
</Style>
</ListView.ItemContainerStyle>
<ListView.Resources>
<Style TargetType="ListViewItem">
<Setter Property="Background" Value="{Binding Color}"/>
</Style>
</ListView.Resources>
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel Width="150" MaxHeight="50" Grid.Column="0" Grid.Row="0" Orientation="Horizontal" >
<TextBlock VerticalAlignment="Center" Text="{Binding Name}" FontWeight="ExtraBlack" TextWrapping="Wrap" Padding="10"/>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
and set the Color property to a brush object
public class ConnectionItem
{
public ConnectionItem(string name)
{
Name = name;
}
public string Name { get; }
private Brush _color = Brushes.Red;
public Brush Color { get => _color; }
private ConnectionStatus _status;
public ConnectionStatus Status
{
set
{
if (value == _status)
{
return;
}
else
{
switch (value)
{
case ConnectionStatus.Connected:
_color = Brushes.Yellow;
break;
case ConnectionStatus.Ready:
_color = Brushes.Green;
break;
default:
_color = Brushes.Red;
break;
}
}
}
}
}
Your ListViewItem style doesn't get applied because your have set the ItemContainerStyle property to another Style. You should move your triggers to the ItemContainerStyle:
<ListView x:Name="lvConnections">
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<EventSetter Event="MouseDoubleClick" Handler="ListViewItem_MouseDoubleClick" />
<Style.Triggers>
<DataTrigger Binding="{Binding Color}" Value="Green">
<Setter Property="Background" Value="Green"/>
</DataTrigger>
<DataTrigger Binding="{Binding Color}" Value="Red">
<Setter Property="Background" Value="Red"/>
</DataTrigger>
<DataTrigger Binding="{Binding Color}" Value="Yellow">
<Setter Property="Background" Value="Yellow"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel Width="150" MaxHeight="50" Grid.Column="0" Grid.Row="0" Orientation="Horizontal" >
<TextBlock VerticalAlignment="Center" Text="{Binding Name}" FontWeight="ExtraBlack" TextWrapping="Wrap" Padding="10"/>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
recently I've tried to make label style which would allow to display image or textblock depending on property being set or not. I've bound proper objects to labels' DataContext and prepared reusable style for these labels. Default content is textblock with Name as its text but if IsIconSet property is true, then content would change into image with corresponding IconPath as source.
Similar approach works perfectly with label's properties like background or cursor but in described scenario it breaks up when IsIconSet has the same value in both instances. Then it displays nothing for first label and correct textblock/image for second label.
I've tried to attach converter to Name and IconPath bindings in style in order to check what value is being passed but it seems that it isn't even invoked on first label.
Has anyone managed to do something similar? Am I missing something fundamental? Or maybe there is another approach for such behaviour?
Any help will be appreciate.
Simplified code:
MainWindow
<StackPanel DataContext="{Binding First}">
<Label Style="{StaticResource LabelStyle}" />
</StackPanel>
<StackPanel DataContext="{Binding Second}">
<Label Style="{StaticResource LabelStyle}" />
</StackPanel>
Style
<Style x:Key="LabelStyle" TargetType="Label">
<Setter Property="Content">
<Setter.Value>
<TextBlock Text="{Binding Name}"/>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding IsIconSet}" Value="True">
<Setter Property="Content">
<Setter.Value>
<Image Source="{Binding IconPath}"/>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
Classes
public class ViewModel : INotifyPropertyChanged
{
private LabelClass _first;
private LabelClass _second;
public LabelClass First
{
get => _first;
set
{
_first = value;
OnPropertyChanged();
}
}
public LabelClass Second
{
get => _second;
set
{
_second = value;
OnPropertyChanged();
}
}
public ViewModel()
{
First = new LabelClass("First", "Resources/first.png");
Second = new LabelClass("Second", "Resources/second.png");
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class LabelClass : INotifyPropertyChanged
{
private string _name;
private string _iconPath;
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged();
}
}
public string IconPath
{
get => _iconPath;
set
{
_iconPath = value;
OnPropertyChanged();
OnPropertyChanged("IsIconSet");
}
}
public bool IsIconSet => !string.IsNullOrEmpty(IconPath);
public LabelClass(string name, string iconPath = null)
{
Name = name;
IconPath = iconPath;
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
LabelStyle can be used by multiple Labels, but TextBlock and Image from Content setter are created just once, there is only one instance for all labels, but it cannot be displayed in multiple places. so it is displayed in only one.
to fix the issue use ContentTemplate, like shown below.
<Setter Property="Content" Value="{Binding}"/> line means that entire DataContext is considered as Label Content. Is is necessary for bindings in ContentTemplate
<Style x:Key="LabelStyle" TargetType="Label">
<Setter Property="Content" Value="{Binding}"/>
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding IsIconSet}" Value="True">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Image Source="{Binding IconPath}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
alternatively, convert TextBlock and Image to non-shared resources:
<Image Source="{Binding IconPath}" x:Key="Img" x:Shared="False"/>
<TextBlock Text="{Binding Name}" x:Key="Txt" x:Shared="False"/>
<Style x:Key="LabelStyle" TargetType="Label">
<Setter Property="Content" Value="{StaticResource Txt}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsIconSet}" Value="True">
<Setter Property="Content" Value="{StaticResource Img}"/>
</DataTrigger>
</Style.Triggers>
</Style>