I am trying to bind visibility of a styled control to the condition that its tag is equal to the selected index of a TabControl.
I use RelativeSource TemplatedParent but it's not being set, and I suspect it's because I am not setting Template property in Style.
Here is my code:
<Grid x:Name="Telas" Grid.Column="1">
<Grid.Resources>
<vw:TagIsIndexBooleanConverter x:Key="TagIsIndexBooleanConverter"/>
<Style x:Key="SelectedIndexVisibleStyle" TargetType="UserControl">
<Setter Property="Visibility" Value="Hidden"/>
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource TagIsIndexBooleanConverter}">
<Binding Path="Tag" RelativeSource="{RelativeSource TemplatedParent}"/>
<Binding Path="SelectedIndex" ElementName="menu"/>
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<vw:TelaColetaView
x:Name="telaColeta"
DataContext="{Binding TelaColetaVM}"
Style="{StaticResource SelectedIndexVisibleStyle}"
Tag="0"/>
<vw:TelaPacientesView
x:Name="telaPacientes"
DataContext="{Binding TelaPacientesVM}"
Style="{StaticResource SelectedIndexVisibleStyle}"
Tag="1"/>
<vw:TelaConfiguraçõesView
x:Name="telaConfigurações"
DataContext="{Binding TelaConfiguraçõesVM}"
Style="{StaticResource SelectedIndexVisibleStyle}"
Tag="4"/>
</Grid>
And converter:
public class TagIsIndexBooleanConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Any(v => v == null || v == DependencyProperty.UnsetValue))
return Binding.DoNothing;
var tag = System.Convert.ToInt32(values[0]);
var index = System.Convert.ToInt32(values[1]);
var result = tag == index;
return result;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
You can't use TemplatedParent in this case because there isn't one; that's meant to used inside a ControlTemplate to specify that the source for the binding is the control that you're applying the template to.
<Binding Path="Tag" RelativeSource="{RelativeSource TemplatedParent}"/>
But this isn't inside a ControlTemplate. Instead, Tag is just a property of the thing you're styling. Ordinarily you'd do a trigger like this for Tag:
<Trigger Property="Tag" Value="0">
...but you need a multibinding to get the SelectedIndex from menu, and it's got to be a MultiDataBinding because you need to specify ElementName.
So you need a binding. To bind to one of your own properties instead of a property of your DataContext, you bind with a RelativeSource of Self:
<Binding Path="Tag" RelativeSource="{RelativeSource Self}" />
OP also found he had to set the TargetType of the Style to "UserControl", for the Tag binding to work.
Related
The background of the column header of the current cell is changed when the CurrentCellChanged event is raised using this code in code-behind:
private void DataGrid_CurrentCellChanged(object sender, EventArgs e)
{
foreach (var item in dataGrid.Columns)
{
if (item.HeaderStyle != null)
{
item.HeaderStyle = null;
}
}
Style selectedColumnStyle = new Style(typeof(DataGridColumnHeader));
selectedColumnStyle.Setters.Add(new Setter(DataGridColumnHeader.BackgroundProperty, Brushes.Gray));
dataGrid.Columns[Index].HeaderStyle = selectedColumnStyle;
}
How to achieve same functionality in XAML using a style and triggers ?
I do not think that you can achieve this purely in XAML...but almost. You could create a column header style that changes the background, which is triggered by determining if the current cell column matches the column of the header that the style is applied to.
<DataGrid ...>
<DataGrid.Resources>
<local:EqualityToBoolConverter x:Key="EqualityToBoolConverter"/>
</DataGrid.Resources>
<DataGrid.ColumnHeaderStyle>
<Style TargetType="{x:Type DataGridColumnHeader}" BasedOn="{StaticResource {x:Type DataGridColumnHeader}}">
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource EqualityToBoolConverter}">
<Binding RelativeSource="{RelativeSource Self}" Path="Column"/>
<Binding RelativeSource="{RelativeSource AncestorType={x:Type DataGrid}}" Path="CurrentCell.Column"/>
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Background" Value="Gray"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.ColumnHeaderStyle>
<!-- ...your markup. -->
</DataGrid>
As you can see, there is a multi binding that binds the column of the header the style is applied to and the column of the current cell. What we still need in code is a converter that checks for equality.
public class EqualityToBoolConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values != null && values.Length > 0 && values.Skip(1).All(value => Equals(values[0], value));
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new InvalidOperationException("This conversion is one-way.");
}
}
The DataTrigger then receives a boolean indicating, whether to apply the setters or not.
I have a ListView, with its items represented by an ItemTemplate like so:
<ListView dependencyObjects:InterestingItem.Interesting="{Binding InterestingItem}"
ItemsSource="{Binding Quotations}" >
<ListView.ItemTemplate>
<DataTemplate>
<Border>
<Grid>
<StackPanel x:Name="NotImportant">
</StackPanel>
<Grid x:Name="HiddenGrid"
Background="Red"
Visibility="Hidden" >
<Grid.Style>
<Style TargetType="Grid">
<Style.Triggers>
<Grid.Triggers>
<DataTrigger Binding="{Binding Path=DataContext.InterestingItem,
RelativeSource={RelativeSource AncestorType={x:Type ListView }}}"
Value="*this instance here*!">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Grid.Triggers>
</Style.Triggers>
</Style>
</Grid.Style>
</Grid>
</Grid>
</Border>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The ListView has an attached property InterestingItem that is one of the items in the ListView.
What I can't hook up is when the InterestingItem is the same as one of the items, the second Grid should become visible.
I would prefer not to change and bind to the actual objects in the list - but rather have the ListView control which item is to be altered.
What is the Value in the DataTrigger that I need?
There are multiple issues in your XAML and conceptually that prevent it from working.
To bind attached properties, you have to use the correct syntax with parentheses.
Path="{Binding (local:InterestingItem.Interesting), RelativeSource={RelativeSource AncestorType={x:Type ListView}}}"
See the Binding path syntax documenation for reference.
The Triggers property does only support EventTriggers, see FrameworkElement.Triggers.
Note that the collection of triggers established on an element only supports EventTrigger, not property triggers (Trigger). If you require property triggers, you must place these within a style or template and then assign that style or template to the element either directly through the Style property, or indirectly through an implicit style reference.
You cannot bind the Value property of DataTrigger, since it is not a dependency property.
You could of course change the bound type to expose a property that indicates if it is a special object or not and bind that in XAML using a DataTrigger, similar to this (where IsSpecial is the new bool property).
<Grid x:Name="HiddenGrid"
Background="Red">
<TextBlock Text="Hidden Grid"/>
<Grid.Style>
<Style TargetType="{x:Type Grid}">
<Setter Property="Visibility" Value="Hidden"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsSpecial}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
</Grid>
If you want to stick to your current approach, you could create a custom IMultiValueConverter that enables binding multiple properties. It would check if all of the bound values are equal and return Visibility.Visible or Visibility.Hidden otherwise. This example uses Linq to check this and supports an arbitrary number of values bound, but there are many other options.
public class EqualityToVisibilityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values is null || values.Length < 2)
return Binding.DoNothing;
return values.Distinct().Count() == 1 ? Visibility.Visible : Visibility.Hidden;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new InvalidOperationException();
}
}
Next, instantiate the converter in the resources of the ListView or any other resource dictionary in scope and bind the Visibility property of the Grid to both the current item (just <Binding/>) and the attached property local:InterestingItem.Interesting with a MultiBinding that uses the converter to convert them to a Visibility.
<ListView local:InterestingItem.Interesting="{Binding InterestingItem}"
ItemsSource="{Binding Quotations}">
<ListView.Resources>
<local:EqualityToVisibilityConverter x:Key="EqualityToVisibilityConverter"/>
</ListView.Resources>
<ListView.ItemTemplate>
<DataTemplate>
<Border>
<Grid>
<StackPanel x:Name="NotImportant">
<TextBlock Text="Not Important"/>
</StackPanel>
<Grid x:Name="HiddenGrid"
Background="Red">
<Grid.Visibility>
<MultiBinding Converter="{StaticResource EqualityToVisibilityConverter}">
<Binding/>
<Binding Path="(local:InterestingItem.Interesting)"
RelativeSource="{RelativeSource AncestorType={x:Type ListView}}"/>
</MultiBinding>
</Grid.Visibility>
<TextBlock Text="Hidden Grid"/>
</Grid>
</Grid>
</Border>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
There are two other things to note here. I have added two dummy TextBlocks, otherwise the result will not be visible, as the panels are empty. Replace them with your content. Furthermore, both the StackPanel and the Grid are overlapping in the parent Grid, I do not know if this is intentional or not, but you can change it by adding rows or columns and moving the elements there.
What is the Value in the DataTrigger that I need?
I am afraid XAML has no support for something like the this keyword in C#.
You may use a MultiBinding with an IMultiValueConverter implementation that determines whether the items are equal:
<Grid x:Name="HiddenGrid" Background="Red">
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="Visibility" Value="Hidden" />
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding>
<MultiBinding.Converter>
<local:MultiConverter />
</MultiBinding.Converter>
<Binding Path="{Binding Path=DataContext.InterestingItem,
RelativeSource={RelativeSource AncestorType={x:Type ListView }}}" />
<Binding Path="{Binding}" />
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
</Grid>
Converter:
public class MultiConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) =>
values != null && values.Length == 2 && values[0] == values[1];
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) =>
throw new NotSupportedException();
}
Note that you cannot set the Visibility property of the Grid to a local value if you want to be able to override the value using a Style setter.
<Grid x:Name="HiddenGrid" Background="Red" Visibility="Hidden">
I have this following style. My problem is that even tho multibinding is always true, I am observing setter only once when Window is loaded. Whenever I change Window Height this MyMultiValueConverter is being called I can see it in logs, but not the "<Setter>".
<Style x:Key="SeperatorRectangleStyle" TargetType="Rectangle">
<Setter Property="Width" Value="2"/>
<Setter Property="Height" Value="50"/>
<Setter Property="MinHeight" Value="49"/>
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource myTheMultiValueConverter}">
<Binding RelativeSource="{RelativeSource AncestorType={x:Type Window}, Mode=FindAncestor}" Path="ActualHeight"/>
<Binding RelativeSource="{RelativeSource AncestorType={x:Type Window}, Mode=FindAncestor}" Path="Name"/>
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Height" Value="{Binding Converter={StaticResource mySeperatorHeightConverter}, ConverterParameter=DataTrigger}"/>
</DataTrigger>
</Style.Triggers>
</Style>
MyMultiValueConverter is:
class TheMultiValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
foreach (var i in values)
{
Console.WriteLine("TheMultiValueConverter values" + i);
}
Console.WriteLine("");
return true
}
}
MySeperatorHeightConverter is
class SeperatorHeightConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Console.WriteLine("!-!- SeperatorHeightConverter: " + parameter);
return 100;
}
}
The problem in your code is that you Bind something to the Height that does not change. That is why the second converter does not get triggered.
See this code:
<Setter Property="Height" Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}, Path=ActualHeight, Converter={StaticResource mySeperatorHeightConverter}, ConverterParameter=DataTrigger}"/>
This small change makes it so that the SeperatorHeightConverter gets triggered everytime the Window Height changes.
I have set up a TreeView with HierarchicalDataTemplate.
I want to set the IsSelected property of a TreeViewItem by Binding so in my ViewModel I have a Property
"ObjectToSelectInTreeView" which holds the object.
In the TreeView I have a Style with multibinding
<Style TargetType="TreeViewItem">
<Setter Property="IsSelected">
<Setter.Value>
<MultiBinding Converter="{StaticResource IsSelectedConverter}" Mode="OneWay">
<Binding ElementName="TreeViewControl" Path="DataContext.DocumentToSelectInTree"
UpdateSourceTrigger="PropertyChanged"></Binding>
<Binding Path="."></Binding>
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Foreground" Value="Black"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsDeleted}" Value="True">
<Setter Property="Foreground" Value="LightGray"/>
</DataTrigger>
</Style.Triggers>
</Style>
And the Converter:
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
ModelBase itemForComparison;
if (values[0] != null)
{
if (values[1] is ObservableDocument)
itemForComparison = ((ObservableDocument)values[1]).Document;
else
itemForComparison = (ModelBase)values[1];
if (values[0] == itemForComparison)
return true;
}
return false;
}
First time all works.
But when I manually select all items in TreeView and then set the ObjectToSelectInTreeView the converter will never run again and so no object is selected.
Thanks for every help!
DataContext.DocumentToSelectInTree property changed is going to trigger the calculation of IsSelectedConverter converter.
I hope you might have implemented INotifyPropertyChanged interface for your ViewModel.
Write the ItemSelectedCommand for the TreeView if you have not written already, and trigger the PropertyChanged Notification for DocumentToSelectInTree property whenever the ItemSelected is executed
I have a list of MyItem. This object represents a shape with some properties, such as width, height and color. Using a converter, I'm trying to change the fill color from (255, r, g, b) to (Alpha, r, g, b) if my object is selected (I do not want to change the opacity). Alpha (double) lives inside my viewmodel.
My problem is that both values sent to the converter are set to DependencyProperty.UnsetValue, no matter what I give to it. If I specify only the Path I would expect it to bind to my viewmodel, but it doesn't. Obviously, I'm doing something wrong here.
Q: I need the Alpha of my viewmodel and the MyItemColor of the DataContext for myitem (which I would assume is still the DataContext when inside the MultiBinding block). What should my Binding tags look like?
XAML
<DataTemplate>
<Grid>
<Rectangle x:Name="myitem" Width="{Binding MyItemWidth}" Height="{Binding MyItemHeight}" />
</Grid>
<DataTemplate.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsSelected}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="myitem" Property="Fill">
<Setter.Value>
<!-- Simple binding works. WHAT is the difference?
<SolidColorBrush>
<SolidColorBrush.Color>
<Binding Path="Color" />
</SolidColorBrush.Color>
</SolidColorBrush>
-->
<SolidColorBrush>
<SolidColorBrush.Color>
<!-- Change fill color if DataContext.Scale changes -->
<MultiBinding Converter="{StaticResource DoubleToColorConverter}">
<!-- wrong? -->
<Binding Path="Alpha" />
<!-- wrong? -->
<Binding ElementName="myitem" Path="MyItemColor" />
</MultiBinding>
</SolidColorBrush.Color>
</SolidColorBrush>
</Setter.Value>
</Setter>
</MultiDataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
Converter
public class DoubleToColorConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType,
object parameter, CultureInfo culture)
{
// values[0] == DependencyProperty.UnsetValue
// values[1] == DependencyProperty.UnsetValue
return null; // New color
}
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
UPDATE
I solved it. I had to look for the parent control (which contains the items) in order to get access to my viewmodel. A clarification of this, why it's good/bad, suffices as an answer! I don't fully understand what's going on :)
<MultiBinding Converter="{StaticResource DoubleToColorConverter}">
<Binding ElementName="myItemsControl" Path="DataContext.Alpha"/>
<Binding Path="Color" />
</MultiBinding>
And...
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values[0] == DependencyProperty.UnsetValue || values[1] == DependencyProperty.UnsetValue)
return Colors.Transparent;
...
}