WPF - HeaderStringFormat Doesn't work in Expander - wpf

I can't seem to find the magic combination to make the HeaderStringFormat work for a WPF Expander.
Here are all the things I've tried:
<Expander Header="{Binding Path=MyProperty, StringFormat=Stuff: ({0})}" >
<TextBlock Text="Some Content" />
</Expander>
<Expander HeaderStringFormat="{}Stuff ({0})" Header="{Binding Path=MyProperty}">
<TextBlock Text="Some More Content" />
</Expander>
<Expander HeaderStringFormat="{}Stuff ({0:0})" Header="{Binding Path=MyProperty}">
<TextBlock Text="Even More Content" />
</Expander>
The only way I can get a formatted string to work correctly in my code is to do this:
<Expander>
<Expander.Header>
<TextBlock Text="{Binding Path=MyProperty, StringFormat=Stuff: ({0})}" />
</Expander.Header>
<Expander.Content>
A Expander with working header
</Expander.Content>
</Expander>
What am I doing wrong?

First thing to note is this:
If you set the HeaderTemplate or
HeaderTemplateSelector property of a
HeaderedContentControl, the
HeaderStringFormat property is
ignored.
MSDN
There are quite a few gotchas like this in WPF to watch out for. You didn't show that in your example, but just keep it in mind. However, I don't think this is your problem.
Second thing to note is that this isn't the same as:
String.Format("My string value is: {0}", myValue");
HeaderedContentControl and HeaderStringFormat are used specifically for classes that implement IFormattable. HederStringFormat formats the header, and ContentStringFormat formats the content. The value of either property is the format that gets passed to your classes implementation if IFormattable.ToString. You can read the full example on MSDN. But here is the gist of how to make it work.
public class MyTestClass : IFormattable
{
#region IFormattable Members
public string ToString(string format, IFormatProvider formatProvider)
{
if(format == "n")
{
return "This is my formatted string";
}
else
{
return "this is my non-formatted string";
}
}
#endregion
}
<Style TargetType="{x:Type TabItem}">
<Setter Property="HeaderStringFormat" Value="n" />
<Setter Property="ContentStringFormat" Value="" />
</Style>
<TabControl>
<TabItem Header="{Binding Content, RelativeSource={RelativeSource Self}}">
<local:MyTestClass />
</TabItem>
</TabControl>
This TabItem will now display "This is my formatted string" in the header, and the content will be "this is my non-formatted string".
There a couple things to keep in mind. Typically these properties would be used only in an HeaderedItemsControl context. The HeaderStringFormat would not be bound in this way, and instead will have the default binding provided by the ItemContainer of the HeaderedItemsControl. For instance if you set the ItemsSource property of the TabItem, then it will automatically wire up the header and the content binding for you, and all you have to do is supply the formatting value you want.
Last, but not least, I was able to get everything working properly with a GroupBox and TabItem, but not so much luck with an expander and I'm not sure why. The expander handles the ContentStringFormat properly, but not the HeaderContentStringFormat. This is suprising considering that the both inherit from HeaderContentControl.

Related

Binding TabControl.ItemsSource to ViewModels whose View's are a TabItem

I need some TabItems to have a customized Header.
For example, given the following (working) XAML:
<TabControl>
<TabItem>
<TabItem.Header>
<Button>Header 1</Button>
</TabItem.Header>
<Label>Content 1</Label>
</TabItem>
<TabItem>
<TabItem.Header>
<Label>Header 2</Label>
</TabItem.Header>
<Grid>
<TextBlock>Content 2</TextBlock>
</Grid>
</TabItem>
</TabControl>
I would like to extract the tab items into their own Views + ViewModels. The TabItem's View should still be a TabItem, so that I can configure the Header per tab item instead of setting TabControl.ItemTemplate and using a DataTemplateSelector to achieve different headers per tab item.
At the same time I'd need to be able to bind the selected tab item view model to a property ActiveItem. => The underlying view-model for the TabControl is a Conductor.Collection.OneActive<T> (only the selected tab should be activated).
If there's an alternative to using TabItem as view-type, but still achieving the Header and Content to be specified in the same view, it would be acceptable, too.
You should be able to achieve this by binding a TabControlViewModel to the TabControl, and that VM should have an ObservableCollection of TabViewModels (maybe a base class or interface). You would bind your collection of TabViewModels to the TabControl's ItemsSource. Here is my implementation, but using the Telerik TabControl (should be same for MS):
<telerik:RadTabControl x:Name="RadTabControl"
Grid.Row="0"
Align="Justify"
ContentTemplateSelector="{StaticResource LoggerDataTemplateSelector}"
IsContentPreserved="True"
IsDefaultItemSelected="True"
ItemsSource="{Binding LogHistory}"
SupressSelectedContentTemplateReapplying="False">
<telerik:RadTabControl.ItemContainerStyle>
<!-- Allow IsSelected to be bound to view models-->
<Style BasedOn="{StaticResource RadTabItemStyle}"
TargetType="{x:Type telerik:RadTabItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</telerik:RadTabControl.ItemContainerStyle>
<telerik:RadTabControl.ItemTemplate>
<!-- Define what is shown in the header -->
<DataTemplate>
<Grid Height="30">
<TextBlock VerticalAlignment="Center"
Text="{Binding Title}" />
</Grid>
</DataTemplate>
</telerik:RadTabControl.ItemTemplate>
</telerik:RadTabControl>
Be aware, MS doesn't have something like IsContentPreserved, so switching tabs that have a lot of data to show will be rather timely. There are also a couple other properties not in MS TabControl, however the important properties should all be there. In this case, your TabViewModel should have a IsSelected property and Title property.

How to retrieve VisualChild from ControlTemplate

I have a WPF application in .NET 3.5 SP1 which is using TabControl.
In that we have TabItems, which in turn have their Styles to determine currently displayed items.
Let's say we have a TabItem named Books, now Books will have three stages of display:
1. Loading results,
2. Displaying results,
3. Displaying no results - i.e. nothing found.
<TabControl>
<TabItem Header="Books"/>
<TabItem Header="DVD's"/>
...
</TavControl>
Now I have 5 TabItems which let's say represent "DVD's", "Blu-Rays", "CD's", "Books" and "Comics".
<TabItem Header="Books">
<Control>
<Control.Resources>
<Style TargetType="Control">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Control">
<ListView ItemsSource="{Binding Books}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<!-- Assign different Visuals depending on the current state of the app, i.e. Loading, No results, results found
<DataTrigger .../>
</Style.Triggers>
</Style>
</Control.Resources>
</Control>
</TabItem>
Underneath the TabItem I have a TextBlock to display number of currently found results:
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="Displaying {0} records for your Shop ({1})" Converter="{StaticResource tstMVC}">
<Binding ElementName="Tc" Path="SelectedValue"/>
<Binding Path="ShopId" FallbackValue="Liverpool"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
Converter is there for me to check what values are passed in the MultiBinding.
ISSUE:
When user selects a tab item I would like to display current number of the items displayed but I can't locate the ListView in the Control, as that's the Current Content of the TabItem.
I have tried TabControl.SelectedItem, SelectedValue and still can't find Current ItemsSource.Count.
Thanks in advance
UPDATE:
I have tried both of the solutions big thanks to #Sheridan and #pushpraj!
Unfortunately I haven't used either of them, instead I used ListView inside of the TabItem and then accessed it with this code:
<TabControl Name="Tc">
<TabItem><ListView ItemsSource="{Binding Books}"/></TabItem>
...
</TabControl>
<TextBlock Text="{Binding ElementName=Tc, Path=SelectedItem.Content.Items.Count}"/>
This way Content of my TextBlock changes every time user selects different Tab.
P.S. Nevertheless I wouldn't have done it without evaluation of both answers.
if you have separate collection classes for all you entities you may use the following approach
define data templates for your collection classes
eg
<DataTemplate DataType="{x:Type l:BookCollection}">
<ListView ItemsSource="{Binding}" />
</DataTemplate>
xaml
<TabControl x:Name="Tc">
<TabItem Header="Books"
Content="{Binding Books}" />
<TabItem Header="DVD's"
Content="{Binding DVDs}" />
</TabControl>
or if you do not have separate collections then use DataTemplate as follows
<TabControl x:Name="Tc">
<TabControl.ItemTemplate>
<DataTemplate>
<ListView ItemsSource="{Binding}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabItem Header="Books"
Content="{Binding Books}" />
<TabItem Header="DVD's"
Content="{Binding DVDs}" />
</TabControl>
then the binding to get the selected tab's items count will be
<TextBlock Text="{Binding SelectedItem.Content.Count, ElementName=Tc}" />
In WPF, we generally work with data elements rather than UI elements. By that, I mean that it is customary to data bind data elements from our code behind or view models to the UI elements in the views, UserControls and Windows.
Therefore, if you have a data collection property named Books in your code behind or view model, then you can simply refer to that collection to find out how many items are in it, rather than trying to find it via the UI controls:
<ListView ItemsSource="{Binding Books}" />
You could even expose the count of items as a separate property and data bind to it directly:
public ObservableCollection<Book> Books
{
get { return books; }
set
{
books = value;
NotifyPropertyChanged("Books");
NotifyPropertyChanged("BookCount");
}
}
public int BookCount
{
get { return Books.Count; }
}
UPDATE >>>
In response to your latest comment, you can find out how to access UI elements from within a ControlTemplate from the How to: Find ControlTemplate-Generated Elements page on MSDN. In short though, you need to access the element that has the ControlTemplate applied (the relevant TabItem in your case) and then you can use the FrameworkTemplate.FindName Method to find the internally declared elements. Take this example from the linked page:
// Finding the grid that is generated by the ControlTemplate of the Button
Grid gridInTemplate = (Grid)myButton1.Template.FindName("grid", myButton1);
// Do something to the ControlTemplate-generated grid
MessageBox.Show("The actual width of the grid in the ControlTemplate: "
+ gridInTemplate.GetValue(Grid.ActualWidthProperty).ToString());

How to display too long text properly in WPF ComboBox

I have a ComboBox that shows text of various lengths. For texts that are not long there is not a problem. For the texts longer than the width of ComboBox I would like to trim the text and add "..." (an ellipsis) at the end to show them properly. The bottom line is that I don't want to change the width of the ComboBox. Does anyone know how to do this?
Use a custom ItemTemplate for your ComboBox, which makes use of a TextBlock with the TextTrimming property set to CharacterEllipsis.
Example:
<ComboBox ItemsSource="..." SelectedValuePath="...">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock
Text="{Binding ...}"
TextTrimming="CharacterEllipsis" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
The answer, as Ross said, is to implement a custom ItemTemplate. However, to make it work properly, you need to do the binding properly.
A note on this method: You cannot set both the DisplayMemberPath and the ItemTemplate, it must be one or the other.
So, for the general case where the display member is the item (such as for a string), you can use binding with no properties to bind to the DataContext of the template:
<ComboBox ItemsSource="..." SelectedValuePath="...">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding }" TextTrimming="CharacterEllipsis" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Or, you can put it in a style.
<Style TargetType="{x:Type ComboBox}">
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="{Binding }" TextTrimming="CharacterEllipsis" />
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
For the case where you want to bind to a specific property of the object, similar to how you would use the DisplayMemberPath property, replace the binding with the binding that you would use to a property on the object that you are binding. So, replace the fourth line in my first example with something like this:
<TextBlock Text="{Binding MyDisplayMemberProperty}" TextTrimming="CharacterEllipsis" />
The binding is in the context of a single item of the type bound to your ComboBox. To make this more explicit, you can do the following:
<DataTemplate DataType="{x:Type namespace:MyItemType}">
<!-- My DataTemplate stuff here -->
</DataTemplate>
This will give you hints for the properties on the object while you are writing code inside the DataTemplate.
You can use TextTrimming CharacterEllipsis or WordEllipsis for the textblocks in your combobox.
Also works with a more complex DataTemplate; however, I had to resort to a DockPanel instead of the standard WrapPanel.
<ComboBox>
<ComboBox.ItemTemplate>
<DataTemplate>
<DockPanel>
<AccessText DockPanel.Dock="Left" Text="{Binding Icon}"/>
<TextBlock Text="{Binding Name}" TextTrimming="CharacterEllipsis" />
</DockPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>

DataGrid column header text wrapping in silverlight

I am using a DataGrid in my silverlight project and I want one column to wrap it's header text. I know using a style for the header might be the answer but I want to know if there is a wrap property for a datagrid column header?
Here is my code:
<data:DataGrid x:Name="gridViewResources"
AutoGenerateColumns="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ItemsSource="{Binding OpportunityResourceDetailList, Mode=TwoWay}" IsReadOnly="True">
<data:DataGrid.Columns>
<data:DataGridTemplateColumn Header="#" Width="Auto">
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding PositionLevel.FullPositionAndLevelName}" />
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
</data:DataGridTemplateColumn>
Thanks!
I haven't found a way to get Silverlight to automatically wrap the header (in much the same way as the TextWrapping property of a TextBlock does). I suspect that it's not possible due to a limitation of the DataGridColumn.Header property:
Use discretion when using objects as header content; not all Silverlight objects are suitable for use within the limited presentation surface that appears for headers.
However, you can 'manually' wrap header text. If you put a newline in the header text, the header text will be broken over two lines at that point. (In XAML, you use the character entity
.) For example, the following header text appears split over three lines:
<sdk:DataGridTextColumn Header="ABCD
EFGH
IJKL" />
There is no specific property on a DataGridColumn to support this but once you have created the right style it is just as easy - simply set the HeaderStyle property for the specific column.
Create a Style resource with a TargetType of DataGridColumnHeader and set the ContentTemplate property to be a DataTemplate containing a TextBlock with the TextWrapping property set to Wrap. I've enclosed the TextBlock within a Grid panel to keep this closer to the default DataTemplate used by the ContentPresenter. Apply this style to a specific column using the HeaderStyle property or to the whole DataGrid using the ColumnHeaderStyle property.
Note that you will need to constrain the width of the Column to something less than the header text for the wrapping to take affect unless you specifically restrict the width of the TextBlock in the DataTemplate.
<Style x:Key="CustomDataGridColumnHeaderStyle" TargetType="sdk:DataGridColumnHeader" BasedOn="{StaticResource DefaultDataGridColumnHeaderStyle}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding}" TextWrapping="Wrap" />
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
...
<sdk:DataGrid >
<sdk:DataGrid.Columns>
<sdk:DataGridTextColumn Header="Header Name" Binding="{Binding Xxx}" Width="80" HeaderStyle="{StaticResource CustomDataGridColumnHeaderStyle}"/>
</sdk:DataGrid.Columns>
</sdk:DataGrid>
here is my code
<DataGrid.ColumnHeaderStyle>
<Style TargetType="ContentControl">
<Setter Property="HorizontalContentAlignment" Value="Center"></Setter>
<Setter Property="ContentTemplate" >
<Setter.Value >
<DataTemplate>
<TextBlock Text="{Binding}" TextWrapping="Wrap" FontWeight="Bold" TextAlignment="Center" LineHeight="20"></TextBlock></DataTemplate>
</Setter.Value>
</Setter>
</Style>
`enter code here`</DataGrid.ColumnHeaderStyle>

Custom WPF tooltip

I want to create a WPF tooltip containing a label for the header of the tooltip and then a textblock containing more detailed text. I've created the following style in a resource dictionary:
<Style x:Key="AppToolTip"
TargetType="ToolTip">
<Setter Property="OverridesDefaultStyle" Value="true" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToolTip">
<StackPanel>
<Label Content="{TemplateBinding Content}" FontWeight="Bold" Background="Blue" Foreground="White">
</Label>
<TextBlock Padding="10" TextWrapping="WrapWithOverflow" Width="200">
</TextBlock>
</StackPanel>
</ControlTemplate>
</Setter.Value></Setter>
</Style>
And can successfully apply this style to a button like so and have the tooltip header appear:
<Button.ToolTip>
<ToolTip Style="{DynamicResource PalletToolTip}">
<Binding Source="{x:Static ResStrings.New}"/>
</ToolTip>
</Button.ToolTip>
What i'm stuck on is how can i set the content of the extra descriptive text from the usage above ? I'm already data binding to the Content property when showing the tooltip header.
Anyone who's read Adam Nathan's WPF Unleashed book will recognise that i'm using his example tooltip XAML but in his case, he's used hard coded strings for the content of the label and textblock. I want to create something that's more reusable and hence want to use data binding to achieve the same effect.
I would inherit a HeaderedToolTip class from ToolTip and add a Header property. I would specify the template for that control much as you've done. Then I would be able to use it like so:
<Button>
<Button.ToolTip>
<HeaderedToolTip Header="My Title" Content="My Content"/>
</Button.ToolTip>
</Button>
Or, with bindings:
<Button>
<Button.ToolTip>
<HeaderedToolTip Header="{Binding ToolTipTitle}" Content="{Binding ToolTipText}"/>
</Button.ToolTip>
</Button>
You can use an object or ViewModel that contains all the necessary properties you need in the tooltip.
class MyToolTipViewModel : INotifyPropertyChanged
{
public string Header
{
get{ return mHeader;}
set{ mHeader = value; RaisePropertyChanged("Header"); }
}
public void RaisePropertyChanged(string aProperty)
{
// .. implementation of INotifyPropertyChanged
}
}
then you can set the tolltip directly on an instance of this class.
myButton.ToolTip = new MyToolTipViewModel();
now after that, your tooltip will just show the full qualified name of the ViewModel class.
What you need now is a DataTemplate, which tells WPF how to convert the class into visual object.
<DataTemplate DataType="{x:Type MyToolTipViewModel}">
<TextBlock Text="{Binding Header}"/>
</DataTemplate>
The DataTemplate need to be placed in the resource tree. In the resource section of a higher level object or directly on the application or window resources level.
Hope that helps.

Resources