Accessing Controls in ListboxItem - c#

I have a ListBox. This ListBox has custom controls. For example, image, textblocks, etc. Initially textblocks are collapsed, image is visible. I want to make the textblocks visible when user enter with mouse and change opacity of image in that item. But I can't access listboxitems controls. I named them but they don't show up.How can I achieve that ? My ListBox XAML codes:
<ListBox SelectionChanged="MoviesDisplay_OnSelectionChanged" ScrollViewer.HorizontalScrollBarVisibility="Disabled" Background="Black" x:Name="MoviesDisplay">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid x:Name="MoviesGrid" Height="355" Width="240" Margin="10,20,20,3" HorizontalAlignment="Left">
<Image x:Name="MoviePoster" Opacity="1" Source="{Binding Poster}"></Image>
<TextBlock Visibility="Collapsed" x:Name="MovieName" FontSize="18" Text="{Binding Name}" Foreground="White" Margin="0,15,0,321"></TextBlock>
<TextBlock Visibility="Collapsed" x:Name="MovieGenre" FontSize="16" Text="" Foreground="White" Margin="0,34,0,302"></TextBlock>
<TextBlock Visibility="Collapsed" x:Name="MovieReleaseDate" FontSize="16" Text="{Binding ReleaseDate}" Foreground="White" Margin="0,53,164,284"></TextBlock>
<materialDesign:RatingBar Visibility="Collapsed" Foreground="White"
Value="{Binding Rating}"
x:Name="MovieRatingBar" Margin="59,314,60,17"
/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True" Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
private async void Window_Loaded(object sender, RoutedEventArgs e)
{
client.GetConfig();
var popularMovies = await client.GetMoviePopularListAsync("en", 1);
PopularMovies.Clear();
foreach (var movie in popularMovies.Results)
{
// ImgListUrls.Add(client.GetImageUrl("w500",movie.PosterPath).AbsoluteUri);
Movie mov = new Movie()
{
Id = movie.Id,
Name = movie.OriginalTitle,
ReleaseDate = movie.ReleaseDate.Value.ToShortDateString(),
Poster = client.GetImageUrl("w500", movie.PosterPath).AbsoluteUri,
Rating = movie.VoteAverage
};
PopularMovies.Add(mov);
}
MoviesDisplay.ItemsSource = PopularMovies;
// DownloadImages(ImgListUrls);
}

You don't need to access these elements in code behind. Add Styles with DataTriggers instead, e.g.:
<Image Source="{Binding Poster}">
<Image.Style>
<Style TargetType="Image">
<Setter Property="Opacity" Value="1"/>
<Style.Triggers>
<DataTrigger
Binding="{Binding IsMouseOver,
RelativeSource={RelativeSource AncestorType=ListBoxItem}}"
Value="True">
<Setter Property="Opacity" Value="0.5"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
Put the TextBlocks in a StackPanel and add a similar Style:
<StackPanel>
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger
Binding="{Binding IsMouseOver,
RelativeSource={RelativeSource AncestorType=ListBoxItem}}"
Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBlock FontSize="18" Text="{Binding Name}" Foreground="White"/>
<TextBlock FontSize="16" Text="{Binding ReleaseDate}" Foreground="White"/>
</StackPanel>

Related

Make element visible when Validation.HasError is true

I have the following xaml markup:
<GroupBox Margin="10" Padding="20" Header="Case 5 - Custom Error Object">
<GroupBox.DataContext>
<local:ViewModel4/>
</GroupBox.DataContext>
<StackPanel>
<Label Name="AdornerElement" Style="{StaticResource ResourceKey=AdornerElementStyle}"/>
<TextBox Text="{Binding Path=UserName, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}"
Margin="0 10"
Validation.ValidationAdornerSite="{Binding ElementName=AdornerElement}">
</TextBox>
</StackPanel>
</GroupBox>
and the following style:
<Style x:Key="AdornerElementStyle" TargetType="Label">
<Setter Property="Visibility" Value="Collapsed"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Label">
<StackPanel Orientation="Horizontal" Background="LightCoral">
<Image Source="/clipart.png" Width="24" Margin="10"/>
<ItemsControl ItemsSource="{Binding ElementName=AdornerElement, Path=(Validation.ValidationAdornerSiteFor).(Validation.Errors)}"
VerticalAlignment="Center">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=ErrorContent.ValidationMessage}"
Style="{StaticResource CustomErrorTypeStyle}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding ElementName=AdornerElement, Path=Validation.HasError}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Everything works fine except Trigger.
If I set up the property "Visibility" to "Visible" initially, then I can see that error messages are shown correctly.
If I use style shown before, then the Label remains to be collapsed.
Please, help me to use triggers correctly to achieve the final result.
This is not how Validation.ValidationAdornerSite works. This attached property just defines the element that will be adorned with the validation error template. By default this is the element that is validating, the Binding.Target to be more precise.
When you set Validation.ValidationAdornerSite the binding engine will automatically set the attached Validation.ValidationAdornerSiteFor property to reference the element that Validation.ValidationAdornerSite was originally set on.
This means that Validation.HasError and other related attached properties are always set on the Binding.Target and not on the adorner site.
That's why your triggers don't work: they are triggering on the Label instead of the TextBox (where the validation/binding error is registered).
To fix it, the DataTrigger must get the Validation.HasErrors attached property value of the Validation.ValidationAdornerSiteFor attached property, which references the original Binding.Target (the TextBox).
<Style x:Key="AdornerElementStyle"
TargetType="Label">
<Setter Property="Visibility"
Value="Collapsed" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Label">
<StackPanel Orientation="Horizontal"
Background="LightCoral">
<Image Source="/clipart.png"
Width="24"
Margin="10" />
<ItemsControl ItemsSource="{Binding ElementName=AdornerElement, Path=(Validation.ValidationAdornerSiteFor).(Validation.Errors)}"
VerticalAlignment="Center">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ErrorContent.ValidationMessage}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding ElementName=AdornerElement, Path=(Validation.ValidationAdornerSiteFor).(Validation.HasError)}"
Value="True">
<Setter Property="Visibility"
Value="Visible" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Remarks
It's not clear why you are using a Label to display the error messages. You could simply define an error template, which is a ControlTemplate that is referenced by the attached Validation.ErrorTemplate property. This would simplify your code significantly.
The following example creates the exact same visual error feedback but without the hassle for an additional Label and associated triggers to manage the visibility of the error messages. All this is all handled by the WPF binding engine.
<Window>
<Window.Resources>
<!-- The Validation.Errors property is the DataContext of this template -->
<ControlTemplate x:Key="ErrorTemplate">
<StackPanel>
<Border BorderBrush="Red"
BorderThickness="1"
Background="LightCoral">
<StackPanel Orientation="Horizontal">
<Image Source="/clipart.png"
Width="24"
Margin="10" />
<ItemsControl ItemsSource="{Binding}"
VerticalAlignment="Center">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ErrorContent.ValidationMessage}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
<Border BorderBrush="Transparent"
BorderThickness="1"
HorizontalAlignment="Left">
<!-- Placeholder for the Binding.Target -->
<AdornedElementPlaceholder x:Name="AdornedElement" />
</Border>
</StackPanel>
</ControlTemplate>
</Window.Resource>
<StackPanel>
<TextBox Text="{Binding Path=UserName, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}"
Validation.ErrorTemplate="{StaticResoucre ErrorTemplate}" />
</StackPanel>
</Window>

How to use WrapPanel that consists of many TextBlocks under DataTrigger Setter

I have a DataGrid that consists of some columns. In one DataGridTemplateColumn, I want to use a condition. If the condition is False, it should display a single bound property. If the condition is True, it should display multiple bound properties (that's what I can't accomplish). How can I use WrapPanel under DataTrigger Setter?
My XAML code:
<DataGrid x:Name="DG_SipList" ItemsSource="{Binding Items3}" Margin="0 8 0 0" CanUserSortColumns="False" CanUserAddRows="False" AutoGenerateColumns="False" VerticalAlignment="Top" HorizontalAlignment="Left" materialDesign:DataGridAssist.CellPadding="13 8 8 8" materialDesign:DataGridAssist.ColumnHeaderPadding="8" IsReadOnly="True" HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible" >
<DataGrid.Resources>
<Style TargetType="TextBlock" x:Key="cfgText">
<Style.Triggers>
<DataTrigger Binding="{Binding Tag, RelativeSource={RelativeSource Self}}" Value="False">
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="FontStyle" Value="Italic"/>
<Setter Property="TextDecorations" Value="Underline"/>
<Setter Property="Foreground" Value="Red"/>
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="TextWrapping" Value="WrapWithOverflow" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style TargetType="TextBlock" x:Key="cfgText2">
<Style.Triggers>
<DataTrigger Binding="{Binding Tag, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="FontStyle" Value="Italic"/>
<Setter Property="Foreground" Value="Red"/>
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="TextWrapping" Value="WrapWithOverflow" />
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Header="START" IsReadOnly="False">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox x:Name="cb_MontajStart" Checked="cb_MontajStart_Checked" Unchecked="cb_MontajStart_Unchecked" IsChecked="{Binding LISTE_MONTAJ_START}" HorizontalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="ID">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=LISTE_KIMLIK}" Tag="{Binding Path=LISTE_MONTAJ_START}" Style="{StaticResource cfgText2}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="PRODUCT" MaxWidth="450">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock>
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding konfTanim}" Value="False">
<Setter Property="Text" Value="{Binding LISTE_URUN}"/>
</DataTrigger>
<DataTrigger Binding="{Binding konfTanim}" Value="True">
<Setter>
<!--This is what I can not combine more than one textblock under Datatrigger Setter-->
<WrapPanel Orientation="Horizontal" MaxWidth="450">
<TextBlock Text="{Binding Path=yeni_ModelTanim}"/>
<TextBlock Text="{Binding Path=MT4}" Tag="{Binding Path=monStd4}" Style="{StaticResource cfgText}"/>
<TextBlock Text="{Binding Path=MT5}" Tag="{Binding Path=monStd5}" Style="{StaticResource cfgText}"/>
<TextBlock Text="{Binding Path=MT6}" Tag="{Binding Path=monStd6}" Style="{StaticResource cfgText}"/>
</WrapPanel>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- ........................................................... -->
<DataGridTemplateColumn x:Name="txt_Configuration" Header="configuration" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<WrapPanel Orientation="Horizontal" MaxWidth="450">
<TextBlock Text="{Binding Path=yeni_ModelTanim}"/>
<TextBlock Text="{Binding Path=MT4}" Tag="{Binding Path=monStd4}" Style="{StaticResource cfgText}"/>
<TextBlock Text="{Binding Path=MT5}" Tag="{Binding Path=monStd5}" Style="{StaticResource cfgText}"/>
<TextBlock Text="{Binding Path=MT6}" Tag="{Binding Path=monStd6}" Style="{StaticResource cfgText}"/>
</WrapPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
In case of multiple different representations of the same type, depending on a flag or other properties, you have to implement a custom DataTemplateSelector and a data template for each representation, e.g.:
<DataTemplate x:Key="KonfTanimFalseTemplate">
<TextBlock Text="{Binding LISTE_URUN}"/>
</DataTemplate>
<DataTemplate x:Key="KonfTanimTrueTemplate">
<WrapPanel Orientation="Horizontal" MaxWidth="450">
<TextBlock Text="{Binding Path=yeni_ModelTanim}"/>
<TextBlock Text="{Binding Path=MT4}" Tag="{Binding Path=monStd4}" Style="{StaticResource cfgText}"/>
<TextBlock Text="{Binding Path=MT5}" Tag="{Binding Path=monStd5}" Style="{StaticResource cfgText}"/>
<TextBlock Text="{Binding Path=MT6}" Tag="{Binding Path=monStd6}" Style="{StaticResource cfgText}"/>
</WrapPanel>
</DataTemplate>
The data template selector checks the konfTanim property and returns the appropriate data template.
public class KonfTanimDataTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (!(container is FrameworkElement frameworkElement) || !(item is YourItemType yourItem))
return null;
var dataTemplateName = yourItem.konfTanim ? "KonfTanimTrueTemplate" : "KonfTanimFalseTemplate";
return (DataTemplate) frameworkElement.FindResource(dataTemplateName);
}
}
You could also pass in the template names via properties to avoid hard-coding them here. This can help to reuse the selector in your application. In your DataGrid, you could add the data templates to the Resources with an instance of the selector and use it in the template column like this:
<DataGrid ItemsSource="{Binding YourItemTypeList}" AutoGenerateColumns="False">
<DataGrid.Resources>
<DataTemplate x:Key="KonfTanimFalseTemplate">
<TextBlock Text="{Binding LISTE_URUN}"/>
</DataTemplate>
<DataTemplate x:Key="KonfTanimTrueTemplate">
<WrapPanel Orientation="Horizontal" MaxWidth="450">
<TextBlock Text="{Binding Path=yeni_ModelTanim}"/>
<TextBlock Text="{Binding Path=MT4}" Tag="{Binding Path=monStd4}" Style="{StaticResource cfgText}"/>
<TextBlock Text="{Binding Path=MT5}" Tag="{Binding Path=monStd5}" Style="{StaticResource cfgText}"/>
<TextBlock Text="{Binding Path=MT6}" Tag="{Binding Path=monStd6}" Style="{StaticResource cfgText}"/>
</WrapPanel>
</DataTemplate>
<local:KonfTanimDataTemplateSelector x:Key="KonfTanimDataTemplateSelector"/>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn CellTemplateSelector="{StaticResource KonfTanimDataTemplateSelector}"/>
</DataGrid.Columns>
</DataGrid>
Isn't there any other way I can do this just using XAML (without C#)?
There is not really anything bad about having a data template selector for complex conditions.
Choosing a DataTemplate Based on Properties of the Data Object
However, often having a flag to determine the type of an object is a sign that the corresponding class should be split into two different types. In this case, you could create a data template with a specific DataType for both types. Unfortunately, automatic data template selection by type works in other items controls, but in DataGrid or more specifically a template column, you would have to use workarounds, which might be more cumbersome than to just create a selector that might as well be reusable in other columns.

listbox inside ScrollViewer - freeze program

I need some advice. We noticed unusual behavior within the ScrollViewer. I have a StackPanel with whom I am more items including ListBox when the StackPanel placed in a ScrollViewer, when loading data to the listbox, a program for a brief moment freezes. When I am alone ListBox but everything works normally, no freezing of the program.
Here is my code:
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel x:Name="tStack" >
<Grid Height="300">
</Grid>
<Grid Height="300">
</Grid>
<ListBox x:Name="ListBox1" ItemsSource="{Binding AlbumsCvs.View, IsAsync=True}"
Style="{StaticResource ListBoxAlbumsTracksStyles}"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingPanel.IsVirtualizingWhenGrouping="True"
VirtualizingStackPanel.VirtualizationMode="Recycling" >
<ListBox.GroupStyle>
<GroupStyle ContainerStyle="{StaticResource AlbumsHeader}" />
</ListBox.GroupStyle>
</ListBox>
</StackPanel>
</ScrollViewer>
<Style x:Key="ListBoxAlbumsTracksStyles" TargetType="{x:Type ListBox}">
<Setter Property="Padding" Value="0,0,0,0" />
<Setter Property="Margin" Value="0,0,0,0" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Background" Value="Transparent"/>
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Hidden" />
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate >
<DockPanel>
<Border Background="#00000000"
Height="36"
Width="{Binding Path=ActualWidth, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ScrollContentPresenter}}}">
<DockPanel>
<TextBlock x:Name="TrackNumber"
DockPanel.Dock="Left" Margin="2,0,5,0"
Text="{Binding TrackNumber}"
VerticalAlignment="Center"
FontSize="13"
MinWidth="17"
Foreground="Black"/>
<DockPanel>
<TextBlock DockPanel.Dock="Left"
Text="{Binding TrackTitle}"
TextAlignment="Left"
FontSize="13"
VerticalAlignment="Center"
HorizontalAlignment="Stretch"
TextTrimming="CharacterEllipsis"
Margin="0,0,2,0"/>
<TextBlock DockPanel.Dock="Right"
Text="{Binding Duration}"
VerticalAlignment="Center"
HorizontalAlignment="Stretch"
TextTrimming="CharacterEllipsis"
Margin="0,0,10,0"
FontSize="13" TextAlignment="Right"/>
</DockPanel>
</DockPanel>
</Border>
</DockPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- GroupItem -->
<Style x:Key="AlbumsHeader" TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Border Width="{Binding Path=ActualWidth, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ScrollContentPresenter}}}" Background="#00000000">
<StackPanel Margin="0,0,0,15">
<StackPanel>
<TextBlock Text="{Binding AlbumName}"
DataContext="{Binding Items}"
Margin="0,5,0,0"
HorizontalAlignment="Stretch"
FontSize="20"
FontWeight="Light"
TextTrimming="CharacterEllipsis"
Foreground="Black"/>
<TextBlock Text="{Binding IdAlbum}"
DataContext="{Binding Items}"
Margin="0,0,0,10"
HorizontalAlignment="Stretch"
TextTrimming="CharacterEllipsis"
Foreground="Black"/>
</StackPanel>
<ItemsPresenter HorizontalAlignment="Stretch"/>
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
code behind:
private async Task AlbumsArtistInformation()
{
if (string.IsNullOrEmpty(ArtistName))
return;
ObservableCollection<AlbumsArtistCollections> _albumsArtistCollections =
new ObservableCollection<AlbumsArtistCollections>();
try
{
var search = await spotifyDataService.GetArtists(ArtistName);
if (search == null) throw new ArgumentNullException(nameof(search));
foreach (var _artist in search.Artists.Items.Take(1))
{
this.IdArtist = _artist.Id;
}
var _artistAlbum = await spotifyDataService.GetArtistsAlbumsAsync(this.IdArtist, AlbumType.All);
if (_artistAlbum == null) throw new ArgumentNullException(nameof(_artistAlbum));
_albumsArtistCollections = _artistAlbum;
}
finally
{
// Unbind to improve UI performance
Application.Current.Dispatcher.Invoke(() =>
{
this.Albums = null;
this.AlbumsCvs = null;
});
Application.Current.Dispatcher.Invoke(() =>
{
this.Albums = _albumsArtistCollections;
});
Application.Current.Dispatcher.Invoke(() =>
{
// Populate CollectionViewSource
this.AlbumsCvs = new CollectionViewSource { Source = this.Albums };
//Group by Album if needed
this.AlbumsCvs.GroupDescriptions.Add(new PropertyGroupDescription("IdAlbum"));
});
}
}
Does anyone know how to solve this problem.
Vertically oriented StackPanel provides an unbounded available space for the the ListBox, when it calls MeasureOverride(Size availableSize) method, during layout. Therefore, the ListBox (which by default uses virtualization) should create the whole items and this is why you program freezes for a moment.
Therefore, use a DockPanel instead:
<DockPanel x:Name="tStack" LastChildFill="True" >
<Grid DockPanel.Dock="Top" Height="300">
</Grid>
<Grid DockPanel.Dock="Top" Height="300">
</Grid>
<ListBox DockPanel.Dock="Bottom" x:Name="ListBox1" ItemsSource="{Binding AlbumsCvs.View, IsAsync=True}"
Style="{StaticResource ListBoxAlbumsTracksStyles}"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingPanel.IsVirtualizingWhenGrouping="True"
VirtualizingStackPanel.VirtualizationMode="Recycling" >
<ListBox.GroupStyle>
<GroupStyle ContainerStyle="{StaticResource AlbumsHeader}" />
</ListBox.GroupStyle>
</ListBox>
</DockPanel>
LastChildFill is true by default. The ListBox should be the last element, in order to fill the space.
As another option, you can set the Height of the ListBox and put the DockPanel in a ScrollViewer or you can consider a Grid with splitters as another option.

WPF - Using a ResourceDictionary to define a generic style DataTrigger depends on Binding value

First of all, I will try to describe the context of what I need.
I have an application, which display a dashboard like this :
Dashboard-with-KPI
On each line of those KPIs, when there is a valueProcessed I want to see :
aTitle : valueToProcess/valueProcessed [%]
But when there isn't any valueProcessed I need this :
aTitle : 0 [n/a]
In order to achieve it, I used a DataTrigger like this :
<StackPanel Grid.Column="1" Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right">
<StackPanel>
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding NbLocationToInstall}" Value="0">
<Setter Property="Visibility" Value="Hidden" />
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBlock Style="{StaticResource TextBlock_Default_Dashboard}" Text="{Binding NbLocationInstalled}"/>
<TextBlock Style="{StaticResource TextBlock_Default_Dashboard_NoSpace}" Text="/" />
<TextBlock Style="{StaticResource TextBlock_Default_Dashboard}" Text="{Binding NbLocationToInstall}"/>
<TextBlock Style="{StaticResource TextBlock_Default_Dashboard}" Text="{Binding PrLocationInstalled, StringFormat={}[{0:0.##} %]}" />
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Hidden" />
<Style.Triggers>
<DataTrigger Binding="{Binding NbLocationToInstall}" Value="1">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBlock Style="{StaticResource TextBlock_Default_Dashboard}" Text="{Binding NbLocationToInstall}"/>
<TextBlock Style="{StaticResource TextBlock_Default_Dashboard}" Text="[n/a]" />
</StackPanel>
</StackPanel>
Displaying a StackPanel depends on a binding value called NbLocationToInstall.
This solution works but, I have a lot of rows of KPI to display, and I don't want to repeat this block (=> n rows mean n block like this, just binding values will change).
So I have decided to create a dictionary, there I set my style :
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Dictionary.xaml"/>
</ResourceDictionary.MergedDictionaries>
<Style x:Key="ValidTrigger" TargetType="StackPanel">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding NbLocationToInstall}" Value="0">
<Setter Property="Visibility" Value="Hidden" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="InvalidTrigger" TargetType="StackPanel">
<Setter Property="Visibility" Value="Hidden" />
<Style.Triggers>
<DataTrigger Binding="{Binding NbLocationToInstall}" Value="1">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</UserControl.Resources>
And now, I can use it like this :
<StackPanel Grid.Column="1" Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Style="{StaticResource ValidTrigger}">
<TextBlock Style="{StaticResource TextBlock_Default_Dashboard}" Text="{Binding NbLocationInstalled}"/>
<TextBlock Style="{StaticResource TextBlock_Default_Dashboard_NoSpace}" Text="/" />
<TextBlock Style="{StaticResource TextBlock_Default_Dashboard}" Text="{Binding NbLocationToInstall}"/>
<TextBlock Style="{StaticResource TextBlock_Default_Dashboard}" Text="{Binding PrLocationInstalled, StringFormat={}[{0:0.##} %]}" />
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Style="{StaticResource InvalidTrigger}">
<TextBlock Style="{StaticResource TextBlock_Default_Dashboard}" Text="{Binding NbLocationToInstall}"/>
<TextBlock Style="{StaticResource TextBlock_Default_Dashboard}" Text="[n/a]" />
</StackPanel>
</StackPanel>
But how can I change the binding value of DataTrigger style ValidTrigger ?
This is valid here for this example, given in the context, but for other rows, I want to pass another binding variable. And there, it is NbLocationToInstall, defined in ResourceDictionary.
Many thanks for any help.
According to the suggestion, I made a review of my code.
First of all, this is the XAML code definition :
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Dictionary.xaml"/>
</ResourceDictionary.MergedDictionaries>
<Style x:Key="ValidTrigger" TargetType="StackPanel">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding ValueToProcess}" Value="0">
<Setter Property="Visibility" Value="Hidden" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="InvalidTrigger" TargetType="StackPanel">
<Setter Property="Visibility" Value="Hidden" />
<Style.Triggers>
<DataTrigger Binding="{Binding ValueToProcess}" Value="0">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</UserControl.Resources>
<!-- Detailed Information -->
<GroupBox Header="{x:Static lang:Resource.DetailedInformation}" Grid.Column="0" Grid.Row="0" Margin="5" Style="{StaticResource GroupBox_Default_Dashboard}">
<StackPanel>
<UniformGrid HorizontalAlignment="Stretch" Columns="2">
<TextBlock Style="{StaticResource TextBlock_Default}" HorizontalAlignment="left" Text="{x:Static lang:Resource.DeploymentTime}" />
<TextBlock Style="{StaticResource TextBlock_Default}" HorizontalAlignment="Right">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0:00} h {1:00;00} min">
<Binding Path="Time.Hours" />
<Binding Path="Time.Minutes" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</UniformGrid>
<ListBox ItemsSource="{Binding KPIs}" BorderBrush="Transparent" Background="Transparent" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="0.45" />
</Grid.ColumnDefinitions>
<TextBlock Style="{StaticResource TextBlock_Default}" HorizontalAlignment="Left" Text="{Binding Label}"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Style="{StaticResource ValidTrigger}">
<TextBlock Style="{StaticResource TextBlock_Default_Dashboard}" Text="{Binding ValueProcessed}"/>
<TextBlock Style="{StaticResource TextBlock_Default_Dashboard_NoSpace}" Text="/" />
<TextBlock Style="{StaticResource TextBlock_Default_Dashboard}" Text="{Binding ValueToProcess}"/>
<TextBlock Style="{StaticResource TextBlock_Default_Dashboard}" Text="{Binding ValueProcessed, StringFormat={}[{0:0.##} %]}" />
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Style="{StaticResource InvalidTrigger}">
<TextBlock Style="{StaticResource TextBlock_Default_Dashboard}" Text="{Binding ValueToProcess}"/>
<TextBlock Style="{StaticResource TextBlock_Default_Dashboard}" Text="[n/a]" />
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</GroupBox>
This is the KPI model :
/// <summary>
/// provide a container to display a simple KPI
/// </summary>
public class KPI
{
// props
public string Label { get; private set; }
public double ValueToProcess { get; private set; }
public double ValueProcessed { get; private set; }
public double Percentage { get; private set; }
// default ctor
public KPI(string label, double valueToProcess, double valueProcessed)
{
this.Label = label;
this.ValueToProcess = valueToProcess;
this.ValueProcessed = valueProcessed;
this.Percentage = (valueToProcess == 0) ? Double.NaN
: ValueProcessed / ValueToProcess * 100;
}
}
This is the declaration :
ObservableCollection<KPI> _KPIs;
public ObservableCollection<KPI> KPIs
{
get { return _KPIs; }
set
{
_KPIs = value;
RaisePropertyChanged("KPIs");
}
}
This is the definition :
KPIs = new ObservableCollection<KPI>()
{
{ new KPI(Lang.Resource.Label1, 5, 0) },
{ new KPI(Lang.Resource.Label2, 0, 0) },
{ new KPI(Lang.Resource.Label3, 0, 0) }
};
And It works perfectly.
I hope it could help anyone else.

How to switch StackPanel items' Panel.ZIndex value with a Control outside?

Maybe this is a stupid asking, but I just wonder how to design this kind of layout:
first of all, I have a MenuList ObservableCollection. So I have to put these menus in to a stackPanel like this:
<Window.Resources>
<DataTemplate x:Key="MenuItemTemplate">
<Button Content="{Binding ContentText}" Panel.ZIndex="{Binding PIndex}" Padding="10" FontSize="20"/>
</DataTemplate>
</Window.Resources>
<ItemsControl ItemsSource="{Binding Menus}" ItemTemplate="{StaticResource MenuItemTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
so it looks like this:
after a while, there's a new bar under this menu added in:
<ItemsControl ItemsSource="{Binding Menus}" ItemTemplate="{StaticResource MenuItemTemplate}">
.......
</ItemsControl>
<Border Background="Green" VerticalAlignment="Top" Panel.ZIndex="10" Height="30" Margin="0,81,0,0"/>
Now it's look like this:
The requirment is, if a button selected, it should on the top of the bar, other unselected buttons should be covered by the bar:
But the buttons' are in a StackPanel, so they're all based on StackPanel's ZIndex.
My question is, how to design such layout in xaml?
I think using the renderTransform is better. No need to hard code PIndex in xaml. See below.
<DataTemplate x:Key="MenuItemTemplate">
<Button Content="{Binding}" Padding="10" FontSize="20" Height="30" Width="100">
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter Property="RenderTransform">
<Setter.Value>
<ScaleTransform CenterX="0.5" CenterY="0.5" ScaleX="1" ScaleY="1.2"></ScaleTransform>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</DataTemplate>
<Border Background="Green" VerticalAlignment="Top" Height="30" Margin="0,150,0,0"/>
<ItemsControl ItemsSource="{Binding Menus}" ItemTemplate="{StaticResource MenuItemTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Code behind:
public ObservableCollection<string> Menus { get; set; }
EDIT:
<DataTemplate x:Key="MenuItemTemplate">
<Button Content="{StaticResource image1}" Height="30" Width="100">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid>
<Border x:Name="Border" Height="{TemplateBinding Height}"
Width="{TemplateBinding Width}" Background="Orange">
</Border>
<ContentPresenter ContentSource="Content" HorizontalAlignment="Stretch" VerticalAlignment="Top"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter Property="RenderTransform" TargetName="Border">
<Setter.Value>
<ScaleTransform CenterX="0.5" CenterY="0.5" ScaleX="1" ScaleY="1.2"></ScaleTransform>
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</DataTemplate>
If the bar is literally just a green bar, you can also put it on the DataTemplate as a separate Stackpanel from the button, and then put a Style.Trigger on the Button's stackpanel to increase the ZIndex OnMouseOver. The bar would look seamless even if it is just repeating.
So instead of:
<DataTemplate x:Key="MenuItemTemplate">
<Button Content="{Binding ContentText}" Panel.ZIndex="{Binding PIndex}" Padding="10" FontSize="20"/>
</DataTemplate>
You will have
<DataTemplate x:Key="MenuItemTemplate">
<Grid>
<StackPanel >
<StackPanel.Style>
<Style TargetType="StackPanel" >
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Panel.ZIndex" Value="3"></Setter>
</Trigger>
<Trigger Property="IsMouseOver" Value="False">
<Setter Property="Panel.ZIndex" Value="1"></Setter>
</Trigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<Button Width="100" Height="100" Content="{Binding ContentText}" Padding="10" FontSize="20" />
</StackPanel>
<StackPanel Panel.ZIndex="2">
<Border Background="Green" VerticalAlignment="Top" Height="30"/>
</StackPanel>
</Grid>
</DataTemplate>
(This doesn't look quite perfect if you run it, but you get the idea)
Dockpanel as our ItemPanelTemplate is the right choice.
Use a Grid with Button and Rectangle (for bottom border) as DataTemplate to show Menu items.
Don't apply margin in DataTemplate's Grid in step2 above. Instead use Grid Padding, Button margin for spacing.
DataTemplate :
<DataTemplate x:Key="MenuItemTemplate">
<Grid Background="DodgerBlue">
<Button Margin="5 5 5 0" HorizontalAlignment="Left" Panel.ZIndex="1" Content="{Binding ContentText}" Padding="1" Width="75" GotKeyboardFocus="Button_GotKeyboardFocus" LostKeyboardFocus="Button_LostKeyboardFocus"/>
<Rectangle Panel.ZIndex="2" Fill="BurlyWood" Height="5" Margin="0 25 0 0"/>
</Grid>
</DataTemplate>
ItemPanelTemplate :
<ItemsPanelTemplate>
<DockPanel Height="30" LastChildFill="True"/>
</ItemsPanelTemplate>
Use the below code as is :
<ItemsControl Panel.ZIndex="10" VerticalAlignment="Top" ItemsSource="{Binding Menus}" ItemTemplate="{DynamicResource MenuItemTemplate}" Margin="0">
<ItemsControl.Resources>
<DataTemplate x:Key="MenuItemTemplate">
<Grid Background="DodgerBlue">
<Button Margin="0" HorizontalAlignment="Left" Panel.ZIndex="1" Content="{Binding ContentText}" Padding="1" Width="75" GotKeyboardFocus="Button_GotKeyboardFocus" LostKeyboardFocus="Button_LostKeyboardFocus"/>
<Rectangle Panel.ZIndex="2" Fill="BurlyWood" Height="5" Margin="0 25 0 0"/>
</Grid>
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<DockPanel Height="30" LastChildFill="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Code-Behind
public IList Menus { get; set; }
public Window1()
{
InitializeComponent();
Menus = new[] { new { ContentText = "Menu1" }, new { ContentText = "Menu2" }, new { ContentText = "Menu3" } };
this.DataContext = this;
}
private void Button_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
Panel.SetZIndex((Button)e.OriginalSource, 12);
}
private void Button_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
Panel.SetZIndex((Button)e.OriginalSource, 1);
}

Categories

Resources