WPF: best practice to combine several viewmodel properties into some bindable value - wpf

I have a viewmodel class that is assigned to my usercontrol
class UserControlViewModel
{
public bool A { get; set; }
public bool B { get; set; }
}
I'd like to bind some color to Background property that depends on A and B viewmodel properties. Something like:
A = true, B = true : Black
A = false, B = false: White
A = true, B = false: Green
A = false, B = true: Red
<UserControl Background="{Binding Path=???}" />
I guess it's possible to create converter for my case that should accept UserControlViewModel instance and convert A and B properties into Brush instance and vise versa.
Or may be I shall create another property that implements conversion logic:
class UserControlViewModel
{
public bool A { get; set; }
public bool B { get; set; }
public Brush MyBrush {
get {
if (A && B) return Brushes.Black;
...
}
}
}
What is the best way to solve my problem?

Use DataTriggers instead:
<UserControl ...>
<UserControl.Style>
<Style TargetType="UserControl">
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding A}" Value="True"/>
<Condition Binding="{Binding B}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter Property="Background" Value="Black"/>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding A}" Value="False"/>
<Condition Binding="{Binding B}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter Property="Background" Value="White"/>
</MultiDataTrigger>
<!-- and so on... -->
</Style.Triggers>
</Style>
</UserControl.Style>
</UserControl>

Related

Use DataTrigger in DataGridTemplateColumn.CellStyle

I have a datagrid as follows,
<DataGrid Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" ItemsSource="{Binding SelectedSet.Rows}" IsSynchronizedWithCurrentItem="True" AutoGenerateColumns="False" CanUserAddRows="False" Style="{StaticResource DataGridStyle2}" HeadersVisibility="Column" SelectedItem="{Binding SelectedItem}" EnableRowVirtualization="False">
<DataGrid.Columns>
<DataGridTemplateColumn Width="30*" Header="{StaticResource RangeColumnHeader}" HeaderStyle="{StaticResource HeaderStyle2}" SortMemberPath="StartValue">
<DataGridTemplateColumn.CellStyle>
<DataTrigger Binding="{Binding SelectedSet.IsDefault}" Value="True">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedSet.IsDefault}" Value="False">
<Setter Property="IsEnabled" Value="True"/>
</DataTrigger>
</DataGridTemplateColumn.CellStyle>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DatGrid>
I Want use " SelectedSet's " IsDefault property to set cell's IsEnabled property.
I tried above code, which is not working.
Classes are defined as follows,
public class UCSetModel : ViewModelBase
{
private Set _SelectedSet;
public Set SelectedSet
{
get
{
return _SelectedSet;
}
set
{
_SelectedSet = value;
RaisePropertyChanged("SelectedSet");
}
}
}
public class Set
{
private ObservableCollection<Markers> _rows;
public ObservableCollection<Markers> Rows
{
get
{
return _rows;
}
set
{
_rows = value;
RaisePropertyChanged("Rows");
}
}
private bool _isDefault;
public bool IsDefault
{
get
{
return _isDefault;
}
set
{
_isDefault = value;
RaisePropertyChanged("IsDefault");
}
}
}
I want to bind to a property of the same 'SelectedSet' that the rows come from.
The markup you have posted won't even compile. This does:
<DataGridTemplateColumn.CellStyle>
<Style TargetType="DataGridCell">
<Style.Triggers>
<DataTrigger Binding="{Binding IsDefault}" Value="True">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGridTemplateColumn.CellStyle>
IsDefault is supposed to be a property of a Row object, i.e. you should remove "Set." from the binding path assuming that a row doesn't have a Set property.
If you want to bind to a property of the same SelectedSet that the rows come from, the binding should be defined like this:
<DataTrigger Binding="{Binding DataContext.SelectedSet.IsDefault, RelativeSource={RelativeSource AncestorType=DataGrid}}" Value="True">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
This worked for me,
<DataTrigger Binding="{Binding Path=DataContext.SelectedSet.IsDefault,ElementName=SetWindow}" Value="True">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>

Databinding to enumeration of a static class

Chaps,
I have a datagrid and am colouring the rows as follows.
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridCell}" >
<Style.Triggers>
<Trigger Property="DataGridCell.IsSelected" Value="True">
<Setter Property="Foreground" Value="Blue" />
<Setter Property="Background" Value="White" />
</Trigger>
<DataTrigger Binding="{Binding ErrorType}" Value="TerminalError">
<Setter Property ="Foreground" Value="Purple"/>
</DataTrigger>
<DataTrigger Binding="{Binding ErrorType}" Value="CriticalError">
<Setter Property ="Foreground" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.Resources>
Currently the colours are hard-coded and I need to change this. I have a singleton class that holds colours for different states and colours may be accessed in the following way:
Color returnedColour = ColourSchemes.Instance.GetColour (CriticalError)
So in the xaml, where I have Value="Red" etc, I wish to source the name from the globally accessible ColourSchemes object instead. Would very much appreciate any words of wisdom.
At first, make sure to use Brush instead of Color what you assign a value to a property like Foreground or Background.
Then you may add an indexer property to your ColourSchemes class, which takes the enum value as key:
public enum ErrorType
{
TerminalError, CriticalError
}
public class ColourSchemes
{
private readonly Dictionary<ErrorType, Brush> brushes =
new Dictionary<ErrorType, Brush>
{
{ ErrorType.TerminalError, Brushes.Orange },
{ ErrorType.CriticalError, Brushes.Red }
};
public Brush this[ErrorType value]
{
get { return brushes[value]; }
}
public static ColourSchemes Instance { get; } = new ColourSchemes();
}
Now you may bind a property like this:
Background="{Binding Source={x:Static local:ColourSchemes.Instance}, Path=[CriticalError]}">
Or in a Setter:
<Setter Property="Background"
Value="{Binding Source={x:Static local:ColourSchemes.Instance}, Path=[CriticalError]}"/>
You may however want to take a look at Dynamic Resources.

How to check if a row has odd number?

I'm trying to set different color for odd rows using XAML.
The datagrid in question has 3 different types of data, which I want to color differently, and simply changing AlternatingRowBackground won't do.
I'm planning on using something like
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Type}" Value="0"/>
<Condition Binding="{Binding IsSelected, RelativeSource={RelativeSource Self}}" Value="False"/>
<Condition Binding="{Binding IsOddRow, RelativeSource={RelativeSource Self}}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter Property="Background" Value="#FFDFE6ED"/>
</MultiDataTrigger>
There doesn't seem to be such a property as IsOddRow. What property should I check instead?
You can set AlternationCount on the DataGrid and then bind to the ancestor DataGridRows attached property ItemsControl.AlternationIndex. If the value is "1" you have an odd row number.
<DataGrid ItemsSource="{Binding ...}"
AlternationCount="2">
<DataGrid.CellStyle>
<Style TargetType="DataGridCell">
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Type}" Value="0"/>
<Condition Binding="{Binding RelativeSource={RelativeSource Self},
Path=IsSelected}"
Value="False"/>
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}},
Path=(ItemsControl.AlternationIndex)}"
Value="1"/>
</MultiDataTrigger.Conditions>
<Setter Property="Background" Value="#FFDFE6ED"/>
</MultiDataTrigger>
<!-- ... -->
</Style.Triggers>
</Style>
</DataGrid.CellStyle>
<!-- ... -->
</DataGrid>
Note that when binding to an attached property, you must put parentheses around the attached property. Path=(ItemsControl.AlternationIndex) will work but Path=ItemsControl.AlternationIndex won't.
Update
You could also create the property IsOddRow through an attached behavior. In the behavior you subscribe to LoadingRow. In the event handler you get the index for the loaded row and check if it is odd or not. The result is then stored in an attached property called IsOddRow which you can bind to.
To start the behavior add behaviors:DataGridBehavior.ObserveOddRow="True" to the DataGrid
<DataGrid ItemsSource="{Binding ...}"
behaviors:DataGridBehavior.ObserveOddRow="True">
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Type}" Value="0"/>
<Condition Binding="{Binding Path=IsSelected,
RelativeSource={RelativeSource Self}}" Value="False"/>
<Condition Binding="{Binding Path=(behaviors:DataGridBehavior.IsOddRow),
RelativeSource={RelativeSource Self}}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter Property="Background" Value="#FFDFE6ED"/>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
</DataGrid>
DataGridBehavior
public class DataGridBehavior
{
#region ObserveOddRow
public static readonly DependencyProperty ObserveOddRowProperty =
DependencyProperty.RegisterAttached("ObserveOddRow",
typeof(bool),
typeof(DataGridBehavior),
new UIPropertyMetadata(false, OnObserveOddRowChanged));
[AttachedPropertyBrowsableForType(typeof(DataGrid))]
public static bool GetObserveOddRow(DataGrid dataGrid)
{
return (bool)dataGrid.GetValue(ObserveOddRowProperty);
}
public static void SetObserveOddRow(DataGrid dataGrid, bool value)
{
dataGrid.SetValue(ObserveOddRowProperty, value);
}
private static void OnObserveOddRowChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
DataGrid dataGrid = target as DataGrid;
dataGrid.LoadingRow += (object sender, DataGridRowEventArgs e2) =>
{
DataGridRow dataGridRow = e2.Row;
bool isOddRow = dataGridRow.GetIndex() % 2 != 0;
SetIsOddRow(dataGridRow, isOddRow);
};
}
#endregion // ObserveOddRow
#region IsOddRow
public static DependencyProperty IsOddRowProperty =
DependencyProperty.RegisterAttached("IsOddRow",
typeof(bool),
typeof(DataGridBehavior),
new PropertyMetadata(false));
[AttachedPropertyBrowsableForType(typeof(DataGridRow))]
public static bool GetIsOddRow(DataGridRow dataGridCell)
{
return (bool)dataGridCell.GetValue(IsOddRowProperty);
}
public static void SetIsOddRow(DataGridRow dataGridCell, bool value)
{
dataGridCell.SetValue(IsOddRowProperty, value);
}
#endregion // IsOddRow
}
I am not sure which grid/row type you are using, so I can't give you the exact property names, however, bind to the row's index (row number) and use a value converter (that returns true) to check if the row is odd or even.
Almost every answer use AlternationCount="2" but I find it a little bit too limiting. On my side I use something like AlternationCount="{ Binding MainData.ProjColl.Count}" in order to number my rows until the end ( take care it starts at 0 ! ).
In this case I need a value Converter as mentioned by #Danny Varod.
I use the converter to alternate the color of the rows ( nearly answering the question )
public class IsEvenConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
bool res = false;
int? val = value as int?;
if (null != val)
res = (0 == (val % 2));
return res;
}
...
}
And the calling XAML
<UserControl ...
<UserControl.Resources>
...
<vm_nmspc:IsEvenConverter x:Key="IsEven"/>
<Style TargetType="DataGridRow">
<Setter Property="Width" Value="Auto"/>
<Setter Property="Background" Value="LightGray"/>
<!--Converter will be used below-->
<Style.Triggers>
...
<Setter Property="Background" Value="LightGray"/
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self},
Path=(ItemsControl.AlternationIndex),
Converter={StaticResource ResourceKey=IsEven}}" Value="true">
<Setter Property="Background" Value="Lavender"/>
</DataTrigger>
<Trigger Property="IsMouseOver" Value="True" >
<Setter Property="Background" Value="LightGreen"/>
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Grid>
<DataGrid ItemsSource="{Binding MainData.ProjColl}" AutoGenerateColumns="False"
AlternationCount="{ Binding MainData.ProjColl.Count}" >
...
<DataGridTextColumn Header="Project Name" ....
</DataGrid>
</Grid>
</UserControl>
and some discrete screenshots
Another part of the same code:
Simple way to display row numbers on WPF DataGrid

WPF DataGridColumnHeader MouseOver - Apply Triggers to the DataGridCell Element

I would like to change background color of that whole datagrid column when I do mouse over of that datagrid column header. Here is code for style I am using.
<Style x:Key="RhinoDataGridBaseStyle" TargetType="{x:Type ctrls:RhinoDataGrid}">
<Style.Resources>
<Style TargetType="{x:Type DataGridCell}">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="White"></Setter>
<Setter Property="BorderThickness" Value="3"></Setter>
<Setter Property="BorderBrush" Value="#4CB7FF"></Setter>
<Setter Property="Foreground" Value="Black"></Setter>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="Height" Value="26"></Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
?????????????????????? What Should I write here ??????????????????????????????
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="{x:Type DataGridRowHeader}">
<Setter Property="Width" Value="36"></Setter>
</Style>
</Style.Resources>
Please help.
Thanks
Its a little tricky, but you could use an attached property to link the cells to their header.
Here is some code I used a while back (excuse it if its ver long)
Here is the attached property file
public static class GroupMessaging
{
private static readonly Dictionary<string, List<DependencyObject>> messageDictionary = new Dictionary<string, List<DependencyObject>>();
public static readonly DependencyProperty MessageKeyProperty = DependencyProperty.RegisterAttached("MessageKey", typeof(string), typeof(GroupMessaging), new PropertyMetadata(null, OnMessageKeyChanged));
public static void SetMessageKey(UIElement element, string value)
{
element.SetValue(MessageKeyProperty, value);
}
public static string GetMessageKey(UIElement element)
{
return (string)element.GetValue(MessageKeyProperty);
}
public static readonly DependencyProperty MessageProperty = DependencyProperty.RegisterAttached("Message", typeof(string), typeof(GroupMessaging), new PropertyMetadata(null, OnMessageChanged));
public static void SetMessage(UIElement element, string value)
{
element.SetValue(MessageProperty, value);
}
public static string GetMessage(UIElement element)
{
return (string)element.GetValue(MessageProperty);
}
private static void OnMessageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var key = d.GetValue(MessageKeyProperty);
if (key == null || !messageDictionary.ContainsKey((string)key))
{
return;
}
messageDictionary[(string)key].ForEach(o =>
{
var old = o.GetValue(MessageProperty);
if (o != d && e.NewValue != old)
{
o.SetValue(MessageProperty, e.NewValue);
}
});
}
private static void OnMessageKeyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.OldValue != null && messageDictionary.ContainsKey((string)e.OldValue))
{
messageDictionary[(string)e.OldValue].Remove(d);
}
if (e.NewValue != null)
{
if (!messageDictionary.ContainsKey((string)e.NewValue))
{
messageDictionary.Add((string)e.NewValue, new List<DependencyObject>());
}
messageDictionary[(string)e.NewValue].Add(d);
}
}
}
and here is my XAML
<DataGrid ItemsSource="{Binding Items}" AutoGenerateColumns="False">
<DataGrid.ColumnHeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="core:GroupMessaging.MessageKey" Value="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Content}" />
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Grid x:Name="headergrid">
<TextBlock Text="{TemplateBinding Content}" />
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="core:GroupMessaging.Message" Value="Active" />
<Setter Property="Background" Value="Aqua" />
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.ColumnHeaderStyle>
<DataGrid.Columns>
<DataGridTemplateColumn Header="Prop1">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid x:Name="templategrid" core:GroupMessaging.MessageKey="Prop1">
<TextBlock Text="{Binding Prop1}" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding ElementName=templategrid, Path=(core:GroupMessaging.Message)}" Value="Active">
<Setter TargetName="templategrid" Property="Background" Value="Aqua" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Prop2">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid x:Name="templategrid" core:GroupMessaging.MessageKey="Prop2">
<TextBlock Text="{Binding Prop2}" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding ElementName=templategrid, Path=(core:GroupMessaging.Message)}" Value="Active">
<Setter TargetName="templategrid" Property="Background" Value="Aqua" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Prop3">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid x:Name="templategrid" core:GroupMessaging.MessageKey="Prop3">
<TextBlock Text="{Binding Prop3}" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding ElementName=templategrid, Path=(core:GroupMessaging.Message)}" Value="Active">
<Setter TargetName="templategrid" Property="Background" Value="Aqua" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Prop4">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid x:Name="templategrid" core:GroupMessaging.MessageKey="Prop4">
<TextBlock Text="{Binding Prop4}" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding ElementName=templategrid, Path=(core:GroupMessaging.Message)}" Value="Active">
<Setter TargetName="templategrid" Property="Background" Value="Aqua" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
and here is my viewmodel
public class MainViewModel
{
public List<Item> Items { get; private set; }
public MainViewModel()
{
Items = new List<Item>();
Items.Add(new Item() { Prop1 = "item1_1",
Prop2 = "item1_2", Prop3 = "item1_3", Prop4 = "item1_4"});
Items.Add(new Item() { Prop1 = "item2_1",
Prop2 = "item2_2", Prop3 = "item2_3", Prop4 = "item2_4"});
Items.Add(new Item() { Prop1 = "item3_1",
Prop2 = "item3_2", Prop3 = "item3_3", Prop4 = "item3_4"});
Items.Add(new Item() { Prop1 = "item4_1",
Prop2 = "item4_2", Prop3 = "item4_3", Prop4 = "item4_4"});
Items.Add(new Item() { Prop1 = "item5_1",
Prop2 = "item5_2", Prop3 = "item5_3", Prop4 = "item5_4"});
}
}
public class Item
{
public string Prop1 { get; set; }
public string Prop2 { get; set; }
public string Prop3 { get; set; }
public string Prop4 { get; set; }
}

Hide DataTrigger if RelativeSource doesn't exist

I want to add a DataTrigger to my base TextBox style so that it sets the foreground color to a different value if it is inside of a DataGridCell that is selected. Here is what my trigger looks like:
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGridCell}}, Path=IsSelected}"
Value="True">
<Setter Property="Foreground"
Value="White" />
</DataTrigger>
</Style.Triggers>
This works great, except that when my TextBox is not in a DataGrid the Binding fails and writes an exception to the output window. How can I prevent this.
I basically want to say if Parent is a DataGridCell then apply this trigger otherwise ignore it.
In general just only apply the style where applicable. If you want implicit application use nested styles:
<Style TargetType="{x:Type DataGrid}">
<Style.Resources>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger
Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGridCell}}, Path=IsSelected}"
Value="True">
<Setter Property="Foreground" Value="White" />
</DataTrigger>
</Style.Triggers>
</Style>
</Style.Resources>
</Style>
If you have other parts which you want to apply to all TextBoxes take out those parts in a serarate style and use BasedOn in the style which applies to the TextBoxes inside the DataGrid.
Edit: MultiDataTrigger seems to return right away if a condition is not met so you can avoid binding errors:
<Style TargetType="{x:Type TextBox}">
<Style.Resources>
<vc:HasAncestorOfTypeConverter x:Key="HasAncestorOfTypeConverter" AncestorType="{x:Type DataGridCell}" />
</Style.Resources>
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition
Binding="{Binding RelativeSource={RelativeSource Self}, Converter={StaticResource HasAncestorOfTypeConverter}}"
Value="True" />
<Condition
Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGridCell}}, Path=IsSelected}"
Value="True" />
</MultiDataTrigger.Conditions>
<Setter Property="Foreground" Value="Red" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
public class HasAncestorOfTypeConverter : IValueConverter
{
public Type AncestorType { get; set; }
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value == null) return false;
DependencyObject current = value as DependencyObject;
while (true)
{
current = VisualTreeHelper.GetParent(current);
if (current == null)
{
return false;
}
if (current.GetType() == AncestorType)
{
return true;
}
}
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
This of course causes quite som overhead so it might not be such a good solution, then again if the RelativeSource-binding fails it also had to go up the tree first.

Resources