Style for TabItem's content - c#

I have TabControl which is used as a main element of my app:
Every tab content would have a title (duplicate to the menu name), separater and content. My XAML code which must correspond to this view should be like this:
<TabControl Style="{StaticResource MyCustomStyle}">
<TabItem Header="Menu 1">
<TabItem.Content>
...
</TabItem.Content>
</TabItem>
<TabItem Header="Menu 2">
<TabItem.Content>
<TextBlock>Content</TextBlock>
</TabItem.Content>
</TabItem>
...
</TabControl>
To avoid duplicating parts of the code, I decided to setup view in custom style.
MyCustomStyle for this:
<Style x:Key="MyCustomStyle" TargetType="{x:Type TabControl}">
<Style.Resources>
<Style TargetType="{x:Type TabItem}">
<Setter Property="Padding" Value="10"/>
<Setter Property="Width" Value="120"/>
<Setter Property="Content">
<Setter.Value>
<StackPanel Margin="10">
<Label FontSize="20" Content="..."/>
<Separator/>
<ContentPresenter ContentSource="..."/>
</StackPanel>
</Setter.Value>
</Setter>
</Style>
</Style.Resources>
<Setter Property="TabStripPlacement" Value="Left"/>
</Style>
The only problem occurs with the modification of content. Label don't want to bind value to the TabItem's header. The same story for ContentPresenter
I tried to use RelativeSource for this, but this is not working:
<Label FontSize="20" Content="{Binding RelativeSource={RelativeSource AncestorType={x:Type TabItem}}, Path=Header}"/>

If I understand correctly, you're very close -- you just want to give it a ContentTemplate instead of setting the Content in the style:
<Style x:Key="MyCustomStyle" TargetType="{x:Type TabControl}">
<Style.Resources>
<Style TargetType="{x:Type TabItem}">
<Setter Property="Padding" Value="10"/>
<Setter Property="Width" Value="120"/>
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Margin="10">
<Label FontSize="20" Content="..."/>
<Separator/>
<ContentControl
Content="{Binding}"
/>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</Style.Resources>
<Setter Property="TabStripPlacement" Value="Left"/>
</Style>
Now, for the Label content. You can't get that via RelativeSource AncestorType because TabItem isn't in the visual tree: If you write a VisualTreeHelper.GetParent() loop, you find that the parent chain hits a bunch of random stuff like Grids and whatnot, then suddenly it's on TabControl.
So what we do instead, is we write a multi-value converter. We give it the DataContext for the DataTemplate -- that's the Content for the TabItem being ContentTemplated -- and the TabControl. Then we look through the TabControl's Items to find the TabItem that has the same Content.
I tried this as a regular valueconverter, just passing in {RelativeSource Self} from the Label, walking the visual tree inside the converter to find the TabControl, and using the Label's DataContext inside the converter to identify the TabItem I wanted. That didn't work because of (I think) virtualization: The DataTemplate is instantiated once and those controls are reused. Since the value of {RelativeSource Self} is the same each time, and I didn't tell the Binding anything about DataContext, the value converter was only invoked for the first TabItem that was ever selected. The multi-value converter fixes that problem by explicitly binding to ., the DataContext.
This breaks if you use ItemsSource to populate the TabControl. In fact, it breaks if you populate the TabControl with anything but TabItems just like you did. But then, you couldn't be setting their Header properties otherwise, and if you were using ItemsSource, you'd have all kinds of good things to bind to and you wouldn't be contemplating nutty expedients like this one.
TabItemHeaderConverter.cs
public class TabItemHeaderConverter : IMultiValueConverter
{
// This is pretty awful, but nobody promised life would be perfect.
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var tc = values[0] as TabControl;
var tabItemContent = values[1];
var tabItem = tc.Items.Cast<TabItem>().FirstOrDefault(ti => ti.Content == tabItemContent);
if (null != tabItem)
{
return tabItem.Header;
}
return "Unknown";
}
public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
And the new XAML:
<Style x:Key="MyCustomStyle" TargetType="{x:Type TabControl}">
<Style.Resources>
<local:TabItemHeaderConverter x:Key="TabItemHeaderConverter" />
<Style TargetType="{x:Type TabItem}">
<Setter Property="Padding" Value="10"/>
<Setter Property="Width" Value="120"/>
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Margin="10">
<Label
FontSize="20"
>
<Label.Content>
<MultiBinding Converter="{StaticResource TabItemHeaderConverter}">
<Binding RelativeSource="{RelativeSource AncestorType=TabControl}" />
<Binding Path="." />
</MultiBinding>
</Label.Content>
</Label>
<Separator />
<ContentControl
Content="{Binding}"
/>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</Style.Resources>
<Setter Property="TabStripPlacement" Value="Left"/>
</Style>

Related

ListBox display multiple selected items

I have here a ListBox which shall show serveral items where some of them are selected.
<ListBox IsEnabled="False" x:Name="SecondaryModelSelector" SelectionMode="Extended" SelectedItem="{Binding Path=DataContext.SelectedSecondaryModels, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContentControl}, Mode=OneWay}" ItemsSource="{Binding Path=DataContext.AvailableModels, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContentControl}}" >
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Label Content="{Binding Name}" Margin="5,0,5,0"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The selected items are set in a ComboBox and thus the ListBox is disabled and Mode=OneWay to disable selection in the ListBox itself.
The Template looks like this:
<Style x:Key="MyListBoxItem" TargetType="ListBoxItem">
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border x:Name="Border" Padding="2">
<ContentPresenter />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="true">
<Setter TargetName="Border" Property="Background" Value="Blue"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="MyListBoxBorder" TargetType="{x:Type ListBox}">
<Setter Property="ItemContainerStyle" Value="{StaticResource MyListBoxItem}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBox">
<Border x:Name="OuterBorder">
<ScrollViewer Margin="0" Focusable="false">
<StackPanel Margin="2" IsItemsHost="True" />
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The problem is now, that I cannot display multiple selected items, i.e. the background does not turn blue. Showing only one selected item is no problem.
Works (but shows only one selected item...)
public Cpu SelectedSecondaryModels
{
get { return SelectedGlobalVariable.SecondaryModels.FirstOrDefault(); }
}
Does not work:
public ObservableCollection<Model> SelectedSecondaryModels
{
get { return new ObservableCollection<Model>(SelectedGlobalVariable.SecondaryModels); }
}
I have no errors, so how can I show multiple selected items?
I tried SelectedItems instead of SelectedItem instead, but this is read-only field.
When I allow to select items in the ListBox than the blue background appears, and binding throws errors as there is no setter method.
The SelectedItem property cannot be bound to a collection of several items to be selected.
And as you have already noticed, SelectedItems is a read-only property.
You could use an attached behaviour to work around this. Please refer to this blog post and this example for more information about how to do this.
You will also find some solutions here:
Bind to SelectedItems from DataGrid or ListBox in MVVM

Style Trigger using tabindex on tabcontrol is not working when itemsource is referenced

I have tabcontrol which uses an observable collection of tabitems as itemsource. I dont want the close button for the first tab alone. So i added the style trigger
<Style TargetType="{x:Type TabItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Grid Background="White">
<Border Name="Border" BorderBrush="#1b9ed2" Margin="6,0,12,0" Background="White">
<ContentPresenter Height="30" x:Name="ContentSite" ContentSource="Header" VerticalAlignment="Bottom" HorizontalAlignment="Center" Margin="5,15,5,-5">
</ContentPresenter>
</Border>
<Button Background="Transparent" BorderBrush="Transparent" Name="TabCloseButton" Click="TabCloseButton_Click" HorizontalAlignment="Right" VerticalAlignment="Bottom" ToolTip="Close" HorizontalContentAlignment="Right" Padding="0">
<materialDesign:PackIcon Kind="Close" Foreground="Gray" HorizontalAlignment="Right"/>
</Button>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="TabIndex" Value="0">
<Setter TargetName="TabCloseButton" Property="Visibility" Value="Collapsed"/>
</Trigger>
......
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="FontSize" Value="12"/>
</Style>
</TabControl.Resources>
It works if i just added the items in xaml
<TabControl Name ="ConnectionsTab">
<TabItem Header="...." TabIndex="0"></TabItem>
<TabItem Header="...." TabIndex="1"></TabItem>
</TabControl>
But it doesn't works when I make the itemsource to the tabcontrol
private ObservableCollection<TabItem> tabItems = new ObservableCollection<TabItem>();
inside constructor
tabItems.Add(new TabItem { Header = "Connections", Content = new ResourceAccountDisplayUserControl(response), TabIndex = 0 });
ConnectionsTab.ItemsSource = tabItems;
I don't know why it does not work. Any idea or code to make it work will be helpful.
First of all, TabIndex is not the index if your TabItem inside the TabControl. So, you need a different way to determine the actual tab position.
If your data source implements IList, you could for example write a MultiValueConverter that determines the position of an item within a collection:
public class MyTabItemIndexConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
// TODO: add some validation to avoid unsupported values
var c = (IList)values[1];
return c.IndexOf(values[0]);
}
public object[] ConvertBack(object values, Type[] targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Side Note: there are probably other ways to determine the index like Determining index of a tabitem.
Then you can use this converter, to store the actual item index somewhere (or use it directly). In this example, I set the TabItem.Tag property to the item index:
<Window.Resources>
<local:MyTabItemIndexConverter x:Key="tabItemIndexConverter"/>
</Window.Resources>
[...]
<TabControl Name="tabControl1" ItemsSource="{Binding}">
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Tag">
<Setter.Value>
<MultiBinding Converter="{StaticResource tabItemIndexConverter}">
<Binding />
<Binding ElementName="tabControl1" Path="ItemsSource" />
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</TabControl.ItemContainerStyle>
[...]
</TabControl>
Now with TabItem.Tag you have a property that you can actually use in order to implement your trigger logic.

Command binding to top-level MenuItem of Menu doesn't work

I'm learning WPF and developing a dynamic Menu which is driven by data binding of it's ItemsSource to an ObservableCollection. To do this I have a simple MenuItemViewModel and a HierarchicalDataTemplate for automatic binding of MenuItems to it.
The problem I have is that Command property doesn't work for top level menu items. Despite it is set, a MenuItem doesn't react on mouse click and doesn't get disabled if Command cannot be executed. Simply it's like it's not getting bound.
For lower level menu items though, it works as intended. I think this should be a problem of my HierarchicalDataTemplate but I can't find it, because as I see there's no code in template which might affect command binding of only top-level MenuItems.
MenuItemViewModel implements INotifyPropertyChanged and contains following public properties:
string Text
Uri ImageSource
ICommand Command
ObservableCollection<MenuItemViewModel> Children
HierarchicalDataTemplate for MenuItem in my Window.Resources is as follows:
<HierarchicalDataTemplate DataType="{x:Type common:MenuItemViewModel}"
ItemsSource="{Binding Path=Children}">
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command"
Value="{Binding Command}" />
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding ImageSource}" />
<TextBlock Text="{Binding Text}" VerticalAlignment="Center"/>
</StackPanel>
</HierarchicalDataTemplate>
Can you please point me on my mistake?
EDIT: Top-level MenuItem doesn't contain any children (i.e. Children collection of associated ViewModel is empty).
Thanks to #sTrenat comment I've come to solution below.
<Menu.Resources>
<!-- cancel sharing of image so Icon will work properly -->
<Image x:Key="MenuIcon" Source="{Binding ImageSource}" x:Shared="False"/>
</Menu.Resources>
<Menu.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}"
BasedOn="{StaticResource {x:Type MenuItem}}">
<Setter Property="Header" Value="{Binding Text}" />
<Setter Property="Icon" Value="{StaticResource MenuIcon}"/>
<Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="ItemsSource" Value="{Binding Children}" />
<!-- centering MenuItem's Header -->
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" Content="{Binding}" />
</DataTemplate>
</Setter.Value>
</Setter>
<!-- setting Icon to null when ImageSource isn't specified -->
<Style.Triggers>
<DataTrigger Binding="{Binding ImageSource}"
Value="{x:Null}">
<Setter Property="Icon" Value="{x:Null}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Menu.ItemContainerStyle>

Bind Expanded & Collapsed event of TreeViewItem to viewmodel

Some time ago i asked here how to bind the expanded event to the viewmodel and came to a solution using AttachedCommandBehavior:
<TreeView Name="tv" ItemsSource="{Binding TreeViewElements}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children, Mode=OneTime}">
<StackPanel Orientation="Horizontal">
<Label Content="{Binding Title, Mode=OneTime}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="local:CommandBehavior.Event" Value="Expanded"></Setter>
<Setter Property="local:CommandBehavior.Command" Value="{Binding DataContext.ExpandCommand, ElementName=tv}"></Setter>
<Setter Property="local:CommandBehavior.CommandParameter" Value="{Binding}"></Setter>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
Now it is necessary to bind the collapsed event, too.
Adding this to the style section like this does not work, only the collapsed event is binded:
<Setter Property="local:CommandBehavior.Event" Value="Expanded"></Setter>
<Setter Property="local:CommandBehavior.Command" Value="{Binding DataContext.ExpandCommand, ElementName=tv}"></Setter>
<Setter Property="local:CommandBehavior.CommandParameter" Value="{Binding}"></Setter>
<Setter Property="local:CommandBehavior.Event" Value="Collapsed"></Setter>
<Setter Property="local:CommandBehavior.Command" Value="{Binding DataContext.CollapseCommand, ElementName=tv}"></Setter>
<Setter Property="local:CommandBehavior.CommandParameter" Value="{Binding}"></Setter>
Then i found an example on the AttachedCommandBehavior homepage to use a collection of behaviors:
<local:CommandBehaviorCollection.Behaviors>
<local:BehaviorBinding Event="MouseLeftButtonDown" Action="{Binding DoSomething}" CommandParameter="An Action on MouseLeftButtonDown"/>
<local:BehaviorBinding Event="MouseRightButtonDown" Command="{Binding SomeCommand}" CommandParameter="A Command on MouseRightButtonDown"/>
</local:CommandBehaviorCollection.Behaviors>
The problem is that adding such a collection in the style section does not work, visual studio gives the error that the behavior property can not be attached to style.
Has anybody an idea how i can bind both events to the viewmodel?
Do it like this:
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="local:CommandBehaviorCollection.Behaviors">
<Setter.Value>
<local:BehaviorBinding Event="MouseLeftButtonDown" Action="{Binding DoSomething}" CommandParameter="An Action on MouseLeftButtonDown"/>
<local:BehaviorBinding Event="MouseRightButtonDown" Command="{Binding SomeCommand}" CommandParameter="A Command on MouseRightButtonDown"/>
</Setter.Value>
</Setter>
</Style>

Windows 8 metro app: styling a button dynamically. C# and Xaml

I have a xaml page with Gridview which has a Button.I am using a grouped Item page. The buttons are generated based on the dataset returned.
10 records will display 10 buttons.
<GridView.ItemTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Left" Width="Auto" Height="Auto" >
<Button Click="ItemView_ItemClick">
<StackPanel Margin="5" >
<TextBlock Tag="cntCustName" Style="{ThemeResource CntNormalTextBlockStyle}" Text="{Binding CUSTOMERNAME }"/>
<TextBlock Tag="cntCatCode" Style="{ThemeResource CntLrgTextBlockStyle}" Text="{Binding CATEGORYCODE}"/>
<TextBlock Tag="cntDay" Style="{ThemeResource CntNormalTextBlockStyle}" Text="{Binding DAY}"/>
</StackPanel>
</Button>
</Grid>
</DataTemplate>
</GridView.ItemTemplate>
I am getting data in a foreach loop. I want to be able to assign the styles of the buttons
dynamically based on some criteria which I have.
foreach (ContactData ContactData in _openContatcs)
{
if (ContactData.CFLAG)
{
//this is an example
Application.Current.Resources["contactSquare"] = ampStyle;
}
group.Items.Add(ContactData);
}
Styles are defined like this in a folder: Assests/Resources/BaseAPStyles.xaml
<Style x:Name="contactSquare" TargetType="Button">
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Height" Value="160"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="Width" Value="160"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid Background="#ffffc600">
<ContentPresenter>
</ContentPresenter>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
My buttons are not showing the styles. How can I achieve this?
use x:key="contactSqaure" to find the style in the application resources.
Then, access the button you would like to style and assign the style to the button like this.
yourButton.Style = Application.Resources.Current["contactSqaure"] as Style;
You can also define styles in C#, or edit the button's dependency properties.
yourButton.Height = 160;
yourButton.VerticalAlignment = VerticalAlignment.Center;
you can access the button in your datatemplate when it loads.
<DataTemplate>
<Button Loaded="Button_Loaded" .../>
....
void Button_Loaded(object sender, RoutedEventArgs args)
{
(sender as Button).Style = yourStyle;
}

Categories

Resources