I have a DataGrid displaying a list of maps with some details. I'm trying to change the foreground colour of the map which is currently active but I'm struggling. I've written the following converter to check if the DataGrid item's value matches the current map's name, but I'm not sure how to pass the name of the current map from XAML.
Converter:
public class StringToBrushConverter : IValueConverter
{
public string CurrentMapName { get; set; }
public Color CurrentMapColor { get; set; }
public Color OtherMapColor { get; set; }
string _value = "";
public StringToBrushConverter(string currentMapName, Color currentMapColor, Color otherMapColor)
{
CurrentMapName = currentMapName;
CurrentMapColor = currentMapColor;
OtherMapColor = otherMapColor;
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
_value = value as string;
return (string)value == CurrentMapName ? new SolidColorBrush(CurrentMapColor) : new SolidColorBrush(OtherMapColor);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return _value; //TODO: Will this work?
}
}
public sealed class RaceMapColorConverter : StringToBrushConverter
{
public RaceMapColorConverter() : base("", new Color { R = 255, G = 0, B = 0 }, new Color { R = 255, G = 255, B = 255 }) { }
}
XAML:
<DataGrid x:Name="MapHistory"
AutoGenerateColumns="False"
IsReadOnly="True"
HeadersVisibility="Column"
GridLinesVisibility="None"
CanUserSortColumns="False"
Style="{StaticResource MapTimerDataGrid}"
CellStyle="{StaticResource MapTimerControlCell}"
ItemsSource="{Binding Plan.Plan}">
<DataGrid.Resources>
<!-- The Binding in CurrentMapName is not valid. It says it can only be set on a dependency property but I'm not sure how to do that -->
<conv:RaceMapColorConverter x:Key="MapColorConverter" CurrentMapName="{Binding CurrentMap.MapName}" CurrentMapColor="Red" OtherMapColor="White" />
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Header="Map" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Label Content="{Binding Map.MapName}" Foreground="{Binding Map.MapName, Converter={StaticResource MapColorConverter}}" Padding="0" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- Other columns removed to shorten -->
</DataGrid.Columns>
</DataGrid>
I'm also not sure how to implement 'ConvertBack()`. Will storing the original value in the converter work? or could a new instance be created for the same control? Am I even going about this the right way? Thought it's also worth mentioning that I'm using Caliburn.Micro just in case it has any tricks that may help. Any help would be appreciated.
As a general rule-of-thumb you should use converters when the data conversion needs to be two-way (i.e. view-model to view and back again) and DataTriggers when it's one-way only. That answers your question about ConvertBack (which in the example you've provided never gets called anyway, in which case you can just return Binding.DoNothing).
In this particular case, however, a converter is warranted because you need to compare two bindings. Instead of writing an application-aware converter for this one very specific task, I would instead opt for a more generic MultiConverter that simply compares two values that you provide it:
public class EqualityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return (values.Length==2) && Object.Equals(values[0], values[1]);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
return new object[] { Binding.DoNothing, Binding.DoNothing };
}
}
Then back in your application you pass both of your bindings into a MultiBinding of this type and use a DataTrigger to change the map color when the result of the comparison is true:
<Label Content="{Binding Map.MapName}">
<Label.Style>
<Style TargetType="{x:Type TextBlock}" BasedOn="{StaticResource {x:Type Label}}">
<Setter Property="Foreground" Value="Red" /> <!-- normal map color -->
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource EqualityConverter}" Mode="OneWay">
<Binding Path="Map.MapName" />
<Binding Path="CurrentMapName" />
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Foreground" Value="White" /> <!-- other map color -->
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
Related
I have converter for applying background color for DataGrid rows based on values in one column. It is working fine and is applying background color "on load". However I decided to dive deeper into WPF and MVVM and try to attach this event to CheckBox, but failed at this point. I am not getting any errors but my CheckBox does not work either. Any suggestions what should be edited? I guess I need to attach Convert event somehow to BacgroundColorBool?
IDToBackgroundConverter.cs:
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
namespace Inspector_FilterTest
{
class IDToBackgroundConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string)
{
if (value.ToString().Trim().StartsWith("7")) return Brushes.DarkSlateBlue;
if (value.ToString().Trim().StartsWith("6")) return Brushes.Peru;
}
// value is not an integer. Do not throw an exception
// in the converter, but return something that is obviously wrong
return Brushes.Firebrick;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
I use it in XAML:
<Window.Resources>
<Inspector_FilterTest:IDToBackgroundConverter x:Key="IDToBackgroundConverter"/>
</Window.Resources>
...
<DataGrid x:Name="DataGrid1" ItemsSource="{Binding MainDataTable}">
<DataGrid.RowStyle>
<Style TargetType="DataGridRow" >
<Setter Property="Background" Value="{Binding YRNRO, Converter={StaticResource IDToBackgroundConverter}}" />
</Style>
</DataGrid.RowStyle>
</DataGrid>
This is working fine and background colors are set (based on starting number in YRNRO) when I am loading my DataGrid. However I would like to be able to control this with CheckBox.
I have created a CheckBox:
ViewModel_Main.cs:
// Binding checkbox background color
private bool _BacgroundColorBool;
public bool BacgroundColorBool
{
get => this._BacgroundColorBool;
set
{
this._BacgroundColorBool = value;
OnPropertyChanged();
// Refresh the DataTable filter expression
EnableRowFiltering();
}
}
XAML:
<CheckBox Style="{StaticResource MyCheckBox}" IsChecked="{Binding BacgroundColorBool}" x:Name="ActiveCustomer_Copy" Content="" HorizontalAlignment="Left" Margin="221,55,0,0" VerticalAlignment="Top"/>
but I don't understand how to connect this all together in order to be able to control "background fill applied"/"background fill not applied" with CheckBox.
EDIT:
Some corrections done to code originally provided by BionicCode (no errors in debugger anymore, I hope this is correct?). In order to get color back to transparent in Datagrid after CheckBox is unchecked pay attention to return Brushes.Transparent;. That is where you can apply "revertBack" logic.
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
namespace Liinos_inspector_FilterTest
{
class IDToBackgroundConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
// The order of the parameters in the 'values' array is equal to the order
// of the input binding as they are defined in the MultiBinding
var isBackgroundEnabled = (bool)values[0];
if (!isBackgroundEnabled)
{
return Brushes.Transparent;
}
if (values[1] is string stringValue)
{
return stringValue.Trim().StartsWith("7")
? Brushes.DarkSlateBlue
: stringValue.Trim().StartsWith("6")
? Brushes.Peru
: Brushes.Firebrick;
}
return Binding.DoNothing;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
One solution, that doesn't require modification of your view model, is to use a MultiBinding with a IMultiValueConverter. A IMultiValueConverter accepts multiple inputs returned from a MultiBinding, to convert them to a single output (and reverse):
First, turn the IValueConverter into a IMultiValueConverter.
It's best practice to throw the exception that describes the cause of the error the best.
The NotImplementedException is primarily intended to notify the developer that he forgot to implement a relevant and referenced method. It's like a 'TODO'. Since you deliberately decided not to implement the ConvertBack member, because you decided not to support the backwards conversion, you should throw a NotSupportedException instead. This notifies the consumer of your type that the method is not supported instead of implying that the author simply forgot to implement it.
class IDToBackgroundConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
// The order of the parameters in the 'values' array is equal to the order
// of the input binding as they are defined in the MultiBinding
var isBackgroundEnabled = (bool) values[0];
if (!isBackgroundEnabled)
{
return Brushes.Transparent;
}
if (values[1] is string stringValue)
{
return stringValue.Trim().StartsWith("7")
? Brushes.DarkSlateBlue
: stringValue.Trim().StartsWith("6")
? Brushes.Peru
: Brushes.Firebrick;
}
return Binding.DoNothing;
}
// Throw a NotSupportedException instead of a NotImplementedException
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
=> throw new NotSupportedException();
}
Setup the MultiBinding:
<CheckBox IsChecked="{Binding BackgroundColorBool}" />
<DataGrid>
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="Background">
<Setter.Value>
<MultiBinding>
<MultiBinding.Converter>
<IDToBackgroundConverter />
</MultiBinding.Converter>
<Binding Path="BackgroundColorBool" />
<Binding Path="YRNRO" />
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</DataGrid.RowStyle>
</DataGrid>
Because the purpose of the logic is to handle the coloring or visuals of the view, you should not implement this logic or store related data in the view model.
View and view model should never mix!
Whether to use the previous example or to eliminate the BackgroundColorBool property in the view model, depends on the the purpose of the CheckBox. Its name suggests a data related purpose ("ActiveCustomer...") and of course could bind to the view model.
But the name of the source property ("BackgroundColor..."), the CheckBox binds to, suggests a merely view related purpose. In this case the property should not be in the view model. Instead move it as a DependencyProperty to the hosting view or bind the CheckBox directly:
<CheckBox x:Name="ActiveCustomer_Copy" />
<DataGrid>
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="Background">
<Setter.Value>
<MultiBinding>
<MultiBinding.Converter>
<IDToBackgroundConverter />
</MultiBinding.Converter>
<Binding ElementName="ActiveCustomer_Copy"
Path="IsChecked" />
<Binding Path="YRNRO" />
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</DataGrid.RowStyle>
</DataGrid>
I want to bind a TextBlock to an ObservableCollection<Log>. Each Log has different Type (enum), each Type should be resulting in different Run's Foreground (e.g. Error is Red, Success is Green).
I've done a quick read to these Q&A:
WPF and ObservableCollection<T>
In WPF how to define a Data template in case of enum?
WPF DataTemplate Binding depending on the type of a property
But my mind got stuck, because, I'm very new to WPF.
This is the class Log and enum Type:
public enum Type
{
Start = 1,
Stop = 0,
Info = 2,
Success = 4,
Error = 8
};
public class Log
{
public Type Type { get; set; }
public string Message { get; set; }
}
...and this is how I created the collection:
public partial class MainWindow : Window
{
ObservableCollection<Log> mLogCollection = new ObservableCollection<Log>();
public ObservableCollection<Log> LogCollection
{
get { return mLogCollection; }
}
public MainWindow()
{
DataContext = this;
mLogCollection.Add(new Log { Type = Log.Type.Error, Message = "Operation failed" });
mLogCollection.Add(new Log { Type = Log.Type.Success, Message = "Operation complete" });
}
How do I make everything like I want so it will be resulting in something like this?:
<TextBlock>
<Run Text="Operation failed" Foreground="Red"/>
<Run Text="Operation complete" Foreground="Green"/>
</TextBlock>
A balanced XAML and code behind solution is preferred rather than just full XAML.
I'm sorry if my explanation is not clear enough, I'm a bit sleepy right now.
Any help would be appreciated.
Here's the easiest way:
<StackPanel Orientation="Vertical">
<ItemsControl
ItemsSource="{Binding LogCollection}"
>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="local:Log">
<TextBlock
Text="{Binding Message}"
x:Name="MessageText"
/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Type}" Value="Error">
<Setter TargetName="MessageText" Property="Foreground" Value="Red" />
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="Success">
<Setter TargetName="MessageText" Property="Foreground" Value="Green" />
</DataTrigger>
<!--
Etc. for the other log type values.
-->
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
But that's pure XAML and you want code behind. #Evk suggested a value converter for the foreground type, and that's a reasonable way to do it.
XAML:
<ItemsControl
ItemsSource="{Binding LogCollection}"
>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="local:Log">
<DataTemplate.Resources>
<local:LogTypeBrushConverter
x:Key="LogTypeBrush"
/>
</DataTemplate.Resources>
<TextBlock
Text="{Binding Message}"
Foreground="{Binding Type, Converter={StaticResource LogTypeBrush}}"
/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
C#. You defined your enum as Log.Type which won't compile, because the Log class has a property named Type. So I renamed the enum to LogType.
public class LogTypeBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Color color = Colors.Black;
switch ((LogType)value)
{
case LogType.Start:
color = Colors.DodgerBlue;
break;
case LogType.Stop:
color = Colors.OrangeRed;
break;
case LogType.Info:
color = Colors.Blue;
break;
case LogType.Success:
color = Colors.Green;
break;
case LogType.Error:
color = Colors.Red;
break;
}
return new SolidColorBrush(color);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Incidentally, DataContext = this is a bad idea in general. On a Window it's relatively harmless, but try it on a UserControl and you start needing bizarre wokarounds. You can leave that out and bind to LogCollection like so:
ItemsSource="{Binding LogCollection, RelativeSource={RelativeSource AncestorType=Window}}"
My model have two independent properties:
enumType DataType
SomeSpec DataSpec
based on value of DataType I would like to interpretate the DataSpec accordantely
<ContentControl DataContext ="{Binding}">
<MultiBinding Converter ="{StaticResource DataContentConverter}">
<Binding Path ="DataType"/>
<Binding Path ="DataSpec"/>
</MultiBinding>
<ContentControl.Style>
<Style TargetType ="ContentControl">
<Style.Triggers>
<DataTrigger Binding ="{Binding DataType}" Value ="Image">
<Setter Property ="Template">
<Setter.Value>
<ControlTemplate>
<Image Source ="{Binding Image, Mode = OneWay}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
I am using the following converter implementation
public class DataViewConverter: IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values[0] as enumType == null) return null;
var selectedType = (enumType)values[0];
var selectedObject = (SomeSpec)values[1];
switch (selectedType )
{
case enumType.Thumbnail:
case enumType.DisplayImage:
{
return new DataContent
{
DataType = ReducedDataType.Image,
Image = SelectedObject.GetBitmapImage()
};
}
case ...
default:
{
return new DataContent
{
DataType = ReducedDataType.Unknown,
Text = "Content Viewer is not implemented!"
};
}
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
return new object[] {};
}
}
Implementing this I expect to see an image in my window, but instaed of it I only see a text MyNameSpace.DataContent
What am I missing?
I think that your DataTrigger's binding is being based off the ContentControl's DataContext.
<DataTrigger Binding ="{Binding DataType}" Value ="Image">
So, it is looking at ContentControl.DataContext.DataType.
I think what you want is for it to look at the DataType property of the ContentControl's Content. Try changing the DataTrigger to this:
<DataTrigger Binding ="{Binding Content.DataType, RelativeSource={RelativeSource Self}}" Value ="Image">
So now it is looking at ContentControl.Content.DataType. The RelativeSource part directs the binding to the ContentControl instead of ContentControl.DataContext.
I have a field in my UI that can either take a date or a string.
I would like to switch on the type of control displayed depending on the data that is loaded in there or being input by the user.
In other words:
DatePicker if the user starts inputting some numbers (or data loaded is a date)
TextBox is a string input (or string loaded)
Can't find yet how to switch. Happy if you had some tip. Thank you!
You would need to use templates depending on a type. To do this you would need either have 2 properties , one with type of a property and another with the actial object (both notifying INotifyPropertyChanged ).
public object YourProperty
{
get
{
return yourProperty;
}
set
{
yourProperty = value;
OnPropertyChanged();
DateTime date;
if(yourProperty is String && DateTime.TryParse((string) yourProperty, out date))
{
YourProperty = date;
}
}
}
private object yourProperty = string.Empty;
//public Type YourPropertyType { get; set; }
You also can create a converter which will return the type of a property, so you can get rid of additional property (commented out above):
public class TypeOfConverter : MarkupExtension, IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return (value == null) ? null : value.GetType();
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
And finally bind a ContentControl to your property and select a template using the converter above:
<ContentControl Content="{Binding YourProperty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<ContentControl.Resources>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=YourProperty,Converter={flowMathTest:TypeOfConverter}}" Value="{x:Type system:DateTime}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<DatePicker SelectedDate="{Binding Content, RelativeSource={RelativeSource AncestorType=ContentControl}, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding Path=YourProperty,Converter={flowMathTest:TypeOfConverter}}" Value="{x:Type system:String}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<TextBox Text="{Binding Content, RelativeSource={RelativeSource AncestorType=ContentControl}, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Resources>
</ContentControl>
Thant should do it.
EDIT: I did not read the second part so the switching between controls should happen as user writes.
What you can do in this case is to change bindings to Mode=TwoWay and UpdateSourceTrigger=PropertyChanged and handle detection of type in code.
By default set YourProperty to string.Empty. Then on every change check if the text entered is already date using DateTime.Parse. If it is, set YourProperty to this date.
So I updated the code above to reflect these changes.
I've got an editable datagrid on a WPF form inside an MVVM application.
There are two possible actions a user can take on this page that cause some data inside one of the rows to change. One of the editable fields - Requested - can cause another property to change, and there's a style trigger dependent on its value:
public bool OverRequested
{
get
{
if(this.Requested > this.Volume)
{
return true;
}
return false;
}
}
And in the XAML:
<DataGridTextColumn Header="Requested" Binding="{Binding Requested}">
<DataGridTextColumn.CellStyle>
<Style TargetType="DataGridCell">
<Style.Triggers>
<DataTrigger Binding="{Binding OverRequested}" Value="true">
<Setter Property="Foreground" Value="Red"/>
<Setter Property="ToolTip" Value="The requested volume is greater than the available volume" />
</DataTrigger>
</Style.Triggers>
</Style>
</DataGridTextColumn.CellStyle>
</DataGridTextColumn>
The other is a button that updates the data item behind the row and changes a value on the item.
Although the datagrid itself is responsive to changes - if you add or remove rows from the underlying ObservableCollection it updates accordingly in the UI - changes to these properties on the items underneath the rows do not update.
I understand that this is by design, in the sense that changes to items inside an observableCollection do not bubble up, and have to be watched for specifically, but I'm struggling to find out how to achieve what I want.
I've tried - as suggested elsewhere - overriding the event handler for CollectionChanged on the ObservableCollection to add a notification alert to the items inside:
private void ObservableCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (var item in e.NewItems)
{
(item as INotifyPropertyChanged).PropertyChanged += new PropertyChangedEventHandler(item_PropertyChanged);
}
}
}
But items inside do not implement the INotifyPropertyChanged interface - they're Entity Framework objects and not ViewModels. So the cast fails.
Is there a way I can implement this? With only two properties to watch, I'd happily do it manually - but even calling OnPropertyChanged("") to update all the properties in the ViewModel doesn't cause those inside the datagrid to refresh.
Maybe you can try it another way, using converters. I have an app that does something like that. I usually need a test app to get this stuff just right, but try this:
<DataGridTextColumn Header="Requested" Binding="{Binding Requested}">
<DataGridTextColumn.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="Foreground">
<Setter.Value>
<MultiBinding Converter="{StaticResource OverRequestedForegroundMultiConverter}">
<Binding Path="Requested" />
<Binding Path="Volume" />
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="ToolTip">
<Setter.Value>
<MultiBinding Converter="{StaticResource OverRequestedTooltipMultiConverter}">
<Binding Path="Requested" />
<Binding Path="Volume" />
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</DataGridTextColumn.CellStyle>
The converters would look something like this:
public class OverRequestedForegroundMultiConverter : IMultiValueConverter
{
#region IValueConverter Members
public object Convert(object[] value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value != null && value.Length == 2)
{
if (value[0] is int && value[1] is int)
{
int requested = (int)value[0];
int volume = (int)value[1];
if (requested > volume)
return Colors.Red;
}
}
return Colors.Gray; // Or whatever color you want
}
public object[] ConvertBack(object value, Type[] targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
public class OverRequestedTooltipMultiConverter : IMultiValueConverter
{
#region IValueConverter Members
public object Convert(object[] value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value != null && value.Length == 2)
{
if (value[0] is int && value[1] is int)
{
int requested = (int)value[0];
int volume = (int)value[1];
if (requested > volume)
return "The requested volume is greater than the available volume";
}
}
return null;
}
public object[] ConvertBack(object value, Type[] targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
Don't forget to add the converters to your app.xaml:
<app:OverRequestedForegroundMultiConverter x:Key="OverRequestedForegroundMultiConverter" />
<app:OverRequestedTooltipMultiConverter x:Key="OverRequestedTooltipMultiConverter" />