Best way to build omplex, conditional binding - c#

Consider I have to style basing on two properties:
<Label Style={Binding IsEnabled, Convert={x:Static IsEnabledToStyleConverter}} />
or
<Label Style={Binding IsRequired, Convert={x:Static IsEnabledToStyleConverter}} />
TO determine wheter use binding with IsEnabled or IsRequired is other property - UseRequried. How Can is choose between those two bindings basin on UseRequired?
I have tried to approaches:
Another value converter
I have create own value converter:
public class ControlToLableStleConverter: IValueConverter
{
public object Convert(object value..)
{
var myCtrl = (MyControl) value;
if (myCtrl.UseRequired)
//return style based on IsRequired property
else
//return style based on IsEnabled property
}
}
But the problem is that style is no changing at IsEnabled or IsRequired changed. Quite obvious, so this solution is out.
DataTrigger
I have also created DataTriggers:
<Label>
<Label.Triggers>
<DataTrigger Binding="{Binding UseRequired}" Value="True">
<Setters>
<Setter Property="Style" Value="{Binding IsRequired ....">
</Setters>
</DataTrigger>
<DataTrigger Binding="{Binding UseRequired}" Value="False">
<Setters>
<Setter Property="Style" Value="{Binding IsEnabled ....">
</Setters>
</DataTrigger>
</Label.Triggers>
</Label>
But Label.Triggers can contain only EventTrigger elements.. What can i do else?

You can use MultiBinding in this case, though it might be quite verbose:
<Label>
<Label.Style>
<MultiBinding Converter="{StaticResource yourConverter}">
<Binding Path="IsEnabled" />
<Binding Path="IsRequired" />
</MultiBinding>
</Label.Style>
</Label>
And converter is then:
public class StyleConverter : IMultiValueConverter {
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
// take some caution here, because values can be null or DependencyProperty.UnsetValue in certain cases
var enabled = (bool) values[0];
var required = (bool) values[1];
// choose style
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}

Set the triggers within the style.
<Style TargetType="{x:Type MyControl}">
<Style.Triggers>
<DataTrigger Binding="{Binding UseRequired}" Value="True">
<Setters>
<!-- Set properties here, not the Style -->
</Setters>
</DataTrigger>
<DataTrigger Binding="{Binding UseRequired}" Value="False">
<Setters>
<!-- Set properties here, not the Style -->
</Setters>
</DataTrigger>
</Style.Triggers>
</Style>
If you can't change properties then your best option is to use a StyleSelector (MSDN).

Put the such logic in the code behind and trigger changes off of UseRequiered (or elsewhere) and trigger all property changes such as
public bool UseRequiered
{
get { return _UseRequried; }
set { _UseRequried = value; DetermineStyle(); }// PropertyChanged in DetermineStyle()
}
public bool IsRequiered
{
get { return _IsRequiered; }
set { _IsRequiered = value; DetermineStyle();} // PropertyChanged in DetermineStyle()
}
public bool IsEnabled
{
get { return _IsEnabled; }
set { _IsEnabled = value; DetermineStyle(); } // Prop change in DetermineStyle
}
private void DetermineStyle()
{
_UseRequiered = { Whatever logic is deemed };
_IsRequired = { Whatever logic is deemed };
_IsEnabled = { Whatever logic is deemed };
OnPropertyChanged("IsRequired");
OnPropertyChanged("IsEnabled");
OnPropertyChanged("UseRequiered");
}

Related

How to bind two Enum property

I'm trying to bind two properties so I can change the background color in DataGrid based on their value. Based on these answers
How to bind an enum to a combobox control in WPF?
best way to bind enum propery in datagrid
I have implemented the advice in my code, but I'm missing something and it doesn't work.
Thanks for any advices.
namespace Example
{
public class ExampleClass
{
private ExampleObject exampleObject;
public ExampleObject ExampleObject { get; set; }
}
}
namespace Object
{
public class ExampleObject
{
private Value value;
public ExampleObject ExampleObject { get; set; }
}
public enum Value
{
High,
Low
}
}
Wpf DataGrid DataTrigger where I am changing the colour
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Style.Triggers>
<DataTrigger Binding="{Binding ExampleObject.Value}" Value="{StaticResource CellConverter}">
<Setter Property="Background" Value="Green">
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding ExampleObject.Value}" Value="{StaticResource CellConverter}">
<Setter Property="Background" Value="Red">
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
CellConvertor class
public class CellConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Value input = ((Value)value);
switch (input)
{
case Value.High:
return "High";
case Value.Low:
return "Low";
default:
return DependencyProperty.UnsetValue;
}
}
}
You must fix the trigger condition in your example.
Additionally, in order to bind to the ExampleObject.Value enum value, the ExampleObject.Value must be a public property:
public class ExampleObject
{
public Value Value {get; set;}
}
In XAML you reference enum values like static variables and constants by using the x:Static markup extension. In fact, C# enum is implemented as a set of constants. When using the x:Static extension, your current value converter CellConverter becomes obsolete:
<!--
In this example the namespace that defines the enum type 'Value'
is assumed to be registered under the XAML alias 'local'
-->
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Style.Triggers>
<DataTrigger Binding="{Binding ExampleObject.Value}"
Value="{x:Static local:Value.Low}">
<Setter Property="Background" Value="Green" />
</DataTrigger>
<DataTrigger Binding="{Binding ExampleObject.Value}"
Value="{x:Static local:Value.High}">
<Setter Property="Background" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
Remarks: in general, if you want to use a value converter, then you must configure the Binding accordingly and assign the IValueConverter instance to the Binding.Converter property. You can't assign a IValueConverter directly to a property to make it convert. The IValueConverter must receive an input that he can convert to produce the output. The input is the value provided by the actual Binding (and that's why Binding has a Binding.Converter property - they always go in tandem).
See Microsoft Docs: DataBinding Overview (Data conversion)
Note, since the property DataTrigger.Value is not a DependencyProperty, you can't define its value via a Binding.
Converter example:
<Window>
<Window.Resources>
<MyValueConverter x:Name="MyValueConverter" />
</Window.Resources>
<SomeObject SomeDependencyProperty="{Binding SourceProperty, Converter={StaticResource MyValueConverter}}" />
</Window>
It's also recommende best practice to define a default enum value to avoid errors. The default value for an enum instance is always 0 which in your case would default to High. Better allow to identify an unset state by adding an explicit 0 value named None or Default:
public enum Value
{
None = 0,
High,
Low
}
See: Microsoft Docs: Enum Design

Change Label Foreground if content matches another property value

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>

How to set style property of an element in WPF

If IsEnabled property is true, I need to set the Style attribute otherwise it shouldn't be set. In the examples I've seen so far, style properties are set but not the style attribute itself. Below code is not working using Triggers.
<TabItem.Style>
<Style TargetType="TabItem">
<Style.Triggers>
<DataTrigger Binding="{Binding IsEnabled}" Value="True">
<Setter Property="Style" Value="DotcomTabItemStyle" />
</DataTrigger>
</Style.Triggers>
</Style>
</TabItem.Style>
Since you're setting the Trigger through Style, changing the Style would also remove the Trigger... Not really sure if that'll work out :P
Anyway, you're making a mistake on your Setter (setting the resource name directly, not through a static or dynamic resource reference). And you don't need a DataTrigger. It should be:
<Trigger Property="IsEnabled" Value="True">
<Setter Property="Style" Value="{StaticResource DotcomTabItemStyle}" />
</Trigger>
But as I said, this won't probably work as intended, since you're trying to modify the Style property from within the current Style...
One way or another, you'll end up adding different Setters for each property, probably either modifying the DotcomTabItemStyle Style you already have, or creating a new one (based on that one, maybe).
EDIT - Or you could use a Converter and bind the Style property to the IsEnabled property.
I've created a reusable Converter for all this kind of situations:
public class ConditionalSetterConverter : IValueConverter
{
public bool Inverse { get; set; }
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
bool flag = (bool)value;
if (flag ^ Inverse)
return parameter;
else
return Binding.DoNothing;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
You use it like this:
<Window>
<Window.Resources>
<converters:ConditionalSetterConverter x:Key="InverseConditionalSetterConverter"
Inverse="True" />
<Style x:Key="DotcomTabItemStyle" TargetType="TabItem">...</Style>
</Window.Resources>
<TabControl>
<TabItem Style="{Binding IsEnabled,
RelativeSource={RelativeSource Mode=Self},
Converter={StaticResource InverseConditionalSetterConverter},
ConverterParameter={StaticResource DotcomTabItemStyle}}" />
</TabControl>
</Window>
EDIT 2 - OR... You could use a Style selector. ItemsControls like TabControl have a property called ItemContainerStyleSelector, of type StyleSelector.
You'd have to create your own class, inheriting StyleSelector, and override the SelectStyle function to include your custom logic there.
Something like this:
public class DotcomTabItemStyleEnabledSelector : StyleSelector
{
private Style style = null;
public override System.Windows.Style SelectStyle(object item, System.Windows.DependencyObject container)
{
var tabItem = container as TabItem;
if (tabItem != null && tabItem.IsEnabled)
{
if (style == null)
style = textBox.TryFindResource("DotcomTabItemStyle") as Style;
return style;
}
return null;
}
}
I've never used Style selectors, so I'm not really sure if this would work out of the box, but at least you get the idea.

WPF MVVM - Datagrid does not update changed properties of child items

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

Binding complex property of model to DataGridTextColumn and using Style to set value displayed

Let's say i have the code below...i have a Field class that holds a state and the actual value to display. I have model that defines an instance of this MyField class named Field1. In by DataGrid i am binding to this Field1 and using a style to display the value as well as color the background if IsStale is true. The problem is neither the value, nor the background is being colored. It appears the problem is that when used as is, the datacontext for the Style is MyData object and not in fact the MyField object, even though i specify the binding as "Field1". The error being printed out is "BindingExpression path error: 'IsStale' property not found on 'object' ''MyDataModel'".
How can properly bind to a complex property in a Datagrid's cell such that i can use multiple attributes of the bound model?
class MyField : BaseModel
{
private bool _isStale;
public bool IsStale
{
get { return _isStale; }
set
{
if (_isStale == value) return;
_isStale = value;
NotifyPropertyChanged("IsStale");
}
}
private double _value;
public double Value
{
get { return _value; }
set
{
if (_value.Equals(value)) return;
_value = value;
NotifyPropertyChanged("Value");
}
}
}
class MyDataModel
{
MyField Field1 {get; set;}
public MyData()
{
Field1 = new Field1();
//when ever underlying property of MyField changes, we need to fire property changed because xaml binds to Field1
Field1.PropertyChanged += (o,e) =>
{
MyField field = o as MyField;
if (field!=null)
NotifyPropertyChanged("Field1");
};
}
}
<DataGridTextColumn Header="Weight" Binding="{Binding Field1}" ElementStyle="{StaticResource DGCellStyle}"/>
Style:
<Style x:Key="DGCellStyle" TargetType="TextBlock">
<Setter Property="Width" Value="Auto"/>
<Setter Property="Text" Value="{Binding Value, Converter={StaticResource NumberFormatConverter}, ConverterParameter=0.00}" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsStale}" Value="True">
<Setter Property="Background" Value="Pink"/>
</DataTrigger>
</Style.Triggers>
</Style>

Categories

Resources