Expander grouping collapses after source reset - c#

I have a listbox with a bit of templating. Groups are represented by expanders. The listbox is linked to the filesystem and each folder gets its own expander. Any time a file is renamed, deleted, etc, the listbox's view is refreshed. This works great but once the refresh is called, each of the expanders collapses. I can't seem to find a good way to keep them open. I saw another question that used binding to solve this for a single expander. The issue with a data binding on the "IsExpanded" is that there are an unknown number of expanders and I have no way of knowing how many there will be, what they will be called, etc at design time. Any ideas?
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Expander VerticalAlignment="Top"
OverridesDefaultStyle="True"
Template="{StaticResource SimpleExpanderTemp}">
<Expander.Header>
<TextBlock VerticalAlignment="Center"
Background="Transparent"
Text="{Binding Path=Name}"
FontFamily="SegoeUI"
FontSize="16"
Foreground="Black"/>
</Expander.Header>
<Expander.Tag>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0.0" Color="#696969" />
<GradientStop Offset="1.0" Color="#474747" />
</LinearGradientBrush>
</Expander.Tag>
<ItemsPresenter/>
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListBox.GroupStyle>

One possible solution is to still use data binding on the IsExpanded property.
Instead of binding to a boolean, bind to a list of booleans and use a ValueConverter to retrieve the appropriate item from the list.
When creating all your expanders, give each one an index number, if you're not already. Then when you bind the IsExpanded property, set the Converter, and set the converter parameter to the index number of the expander. Then your converter will receive the list of boolean values as the 'value' argument and the index number as the 'parameter' argument and your converter can then return a boolean value.
Your converter might look like this:
public class ListToBooleanConverter : IValueConverter
{
public object Convert(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if ((value != null) & (parameter != null)) {
try {
Int16 itmNum = Convert.ToInt32(parameter);
List<bool> lst = value;
return lst[itmNum];
} catch (Exception ex) {
return null;
}
}
return null;
}
public object ConvertBack(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException("This method or operation is not implemented.");
}
}
In XAML, the implementation of this data binding and converter would look like this (for the expander with an index number of 5):
IsExpanded="{Binding Path=ListIsExpanded, Converter={StaticResource ListToBooleanConverter}, ConverterParameter=5}">
Obviously, in code, this implementation will look a little different.

Related

How to manually add element to a bound Combo Box but not add it to the Collection?

I have a ComboBox that has an ItemsSource bound to an ObservableCollection. I would like to have a NONE value appended to the front of Collection, on the View only. I do not want to actually add this appended value to the bound Collection, only for the ComboBox items. I have been trying ComboBoxItems but that doesn't seem to work, as it won't actually add.
This is my XAML:
<ContentControl Content="{Binding Developer.Games}" Height="25" Width="177">
<ContentControl.ContentTemplate>
<DataTemplate>
<Grid>
<ComboBox x:Name="cb" ItemsSource="{Binding}" DisplayMemberPath="Date"/>
<TextBlock x:Name="tb" Text="Select Game" IsHitTestVisible="False" Visibility="Hidden" TextAlignment="Center" VerticalAlignment="Center"/>
</Grid>
<DataTemplate.Triggers>
<Trigger SourceName="cb" Property="SelectedItem" Value="{x:Null}">
<Setter TargetName="tb" Property="Visibility" Value="Visible"/>
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
So, I would want my Collection to look like this on the expanded drop-down:
NONE
Game1
Game2
Game3
I do not want to actually add this appended value to the bound Collection
The short answer is what you bind to your data source is what you see from the control.
With that in mind, what you should do in your case is simply not bind the data source from the outside, but make a new data source with your extra items internally (meaning inside the custom derived class you should be working on) and bind that instead. You just have to make sure you keep your data source in sync with the outer data source, using the standard observable events.
WPF has a way of changing values between the VM and the view using IValueConverter. You can do something like this:
public class GameListEmptyOptionValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var v = value as ObservableCollection<Game>;
v?.Insert(0, new Game() { Name = "NONE" });
return v;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value;
}
}
and, to use it, on your view:
<Window.Resources>
<local:GameListEmptyOptionValueConverter x:Key="GameListEmptyOptionValueConverter"/>
</Window.Resources>
<Grid>
<ComboBox ItemsSource="{Binding ., Converter={StaticResource GameListEmptyOptionValueConverter}}" DisplayMemberPath="Name"/>
</Grid>
You can use a CompositeCollection.
Here is an example based on the XAML code you provided, that adds a ListBoxItem with the text "NONE" as the first item and displays the rest of the objects in the view model.
<ComboBox x:Name="cb">
<ComboBox.Resources>
<CollectionViewSource x:Key="GamesCollection" Source="{Binding}"/>
</ComboBox.Resources>
<ComboBox.ItemsSource>
<CompositeCollection>
<ListBoxItem>NONE</ListBoxItem>
<CollectionContainer Collection="{Binding Source={StaticResource GamesCollection}}"/>
</CompositeCollection>
</ComboBox.ItemsSource>
</ComboBox>

Binding ListBox.GroupStyle.ContainerStyle

I have a XAML Code like so
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Expander IsExpanded="True" Background="Transparent">
<Expander.Header>
<StackPanel>
<TextBlock Text="{Binding ItemCount}" Foreground="{Binding}"></TextBlock>
</StackPanel>
</Expander.Header>
<ItemsPresenter/>
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListView.GroupStyle>
At <TextBlock Text="{Binding ItemCount}" Foreground="{Binding}"></TextBlock> I don't see any property ItemCount in DataContext of the window and the ListView. So Where does ItemCount came from? When I press F12 to navigate the code Visual Studio don't find definition.
Please explain where it does come from and is it a Property often used?
If there's no Source or RelativeSource for the Binding, the source is the DataContext. Obviously the DataContexts of the window and the listview have nothing to do with it. You're binding to the DataContext of the GroupItem, not the window.
Write a simple pass-through converter:
public class PassThroughConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// Set breakpoint here
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value;
}
}
Use it in your group style template: {Binding Converter={StaticResource PassThrough}}. Just throw that on anything; we don't care about what it returns. We care about getting the DataContext in a watch window where we can poke at it.
Set that breakpoint, use the debugger, and you'll find that the runtime type of the DataContext is MS.Internal.Data.CollectionViewGroupInternal.
Paste that into Google. Look what turns up.
It's a subclass of CollectionViewGroup: Items, ItemCount, Name, etc.
Now you can get rid of the converter, it was only an investigative tool.
It's definitely not a brush. Whatever Foreground="{Binding}" was meant to accomplish, you'll have to rethink it a bit.

Show shortened file path in ComboBox, but full path in ComboBox dropdown

Im trying to figure out how I could display a different string in the ComboBox selection than what is displayed in the dropdown of the ComboBox.
<ComboBox SelectedItem="{Binding LocalFolderSelection, Mode=OneWayToSource}"
ItemsSource="{Binding LocalFolders}"
SelectedIndex="0"/>
Thats my combobox. LocalFolders contains a list of string which are basically filepaths. Due to limited UI space, I cant make the ComboBox very wide so it could display the whole string. The dropdown scales automatically to fit the whole paths which is fine, but I need to reduce the displayed selection to just the filename.
Any ideas how I could achieve this?
I was hoping there would be some property which I could use to define the display text, and maybe bind it to the SelectedItem with a converter that clips off the path, but so far I havent found anything like that.
I believe I have what you are looking for working. Using a Converter with a custom ItemTemplate we can display the shortened path and the ContentPresenter has been modified to preserve the full path.
XAML:
<Window.Resources>
<converters:ShortenFilePathConverter x:Key="ShortenFilePathConverter" />
</Window.Resources>
...
<ComboBox SelectedItem="{Binding LocalFolderSelection}"
ItemsSource="{Binding LocalFolders}">
<ComboBox.ItemTemplate>
<DataTemplate>
<Label Content="{Binding Converter={StaticResource ShortenFilePathConverter}}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
<ComboBox.ItemContainerStyle>
<Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ComboBoxItem}">
<Border x:Name="Bd"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}">
<StackPanel Orientation="Horizontal">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
<ContentPresenter.Content>
<Label Content="{Binding}"/>
</ContentPresenter.Content>
</ContentPresenter>
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
Converter:
public sealed class ShortenFilePathConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
return null;
var s = value as string;
if (s == null)
return null;
return Path.GetFileName(s);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}

Cannot Make Binding to ZIndex from Another Element in WPF

I want to make binding to the ZIndex of another element in WPF in the same .xaml file but it does not work.
The element to be bound.
<Border
x:Name="BubbleTop"
CornerRadius="5"
Background="#EBF5EB"
Padding="8,6"
BorderBrush="LightGray"
BorderThickness="1"
Grid.ZIndex="3">
<ContentPresenter />
</Border>
The element who initial a binding.
<TextBlock
x:Name="statusText"
Margin="..."
Foreground="{Binding ElementName=BubbleTop, Path=Grid.ZIndex, Converter={StaticResource ToggleColorConverter}}"
FontWeight="Bold"
Text="..."/>
In the converter, it is set to change the Foreground color according to the ZIndex of the Border element.
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
int z = (int)value;
if (z == 3)
return "Red";
else
return "Blue";
}
But it does not work. Any hint?
The converter you have would work fine, except that the Path for your binding is wrong. When binding to an attached property, you have to put the path in parens for the path to be parsed correctly.
That said, I don't think a converter really makes much sense here. You can use styling to address a simple toggle like this. This allows you to keep more of the view logic in XAML.
For example:
<TextBlock
x:Name="statusText"
Margin="..."
FontWeight="Bold"
Text="...">
<TextBlock.Style>
<p:Style TargetType="TextBlock">
<Setter Property="Foreground" Value="Blue"/>
<p:Style.Triggers>
<DataTrigger Binding="{Binding ElementName=BubbleTop, Path=(Grid.ZIndex)}" Value="3">
<Setter Property="Foreground" Value="Red"/>
</DataTrigger>
</p:Style.Triggers>
</p:Style>
</TextBlock.Style>
</TextBlock>
(Note: you can omit the p: XML namespace for the <Style/> element. I include that only because the Stack Overflow code formatter gets confused when there's a plain <Style/> element in XML and won't format the XML correctly.)
Try this in your value converter:
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
int z = (int)value;
if (z == 3)
return Brushes.Red;
else
return Brushes.Blue;
}

Automatic tooltip when text is trimmed in DataGrid

My application runs with C# and WPF (.net framework 4.0). My goal is to have a DataGrid in which the text in the cells is trimmed with an ellipsis, and automatically has a tooltip with the full text displayed only if the text in the cell is actually trimmed.
Solution 1: I'm currently using this to know if the text is trimmed or not: http://tranxcoder.wordpress.com/2008/10/12/customizing-lookful-wpf-controls-take-2/
The problem is that it only works when I resize the columns. The tooltips don't show up when the DataGrid is first loaded, when the columns are sorted, or when the ItemSource of the DataGrid is updated.
Solution 2: I've also tried this:http://www.scottlogic.com/blog/2011/01/31/automatically-showing-tooltips-on-a-trimmed-textblock-silverlight-wpf.html
But the tooltips never appear on my DataGrid cells, while it works fine with isolated textblocks.
I'm looking for simple ways to improve Solution 1 and make it work in my DataGrid in all cases, or maybe a different approach.
The style for Solution 1:
<UserControl.Resources>
<Style x:Key="TextColumnElementStyle" TargetType="TextBlock" BasedOn="{StaticResource TextBlockService}">
<Style.Setters>
<Setter Property="TextWrapping" Value="NoWrap" />
<Setter Property="TextTrimming" Value="WordEllipsis" />
</Style.Setters>
</Style>
</UserControl.Resources>
The source code of the TextBlockService
The DataGrid for Solution 1:
<DataGrid ItemsSource="{Binding IssueList}" tbs:TextBlockService.AutomaticToolTipEnabled="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Description" Binding="{Binding Description}"
ElementStyle="{StaticResource TextColumnElementStyle}">
</DataGrid.Columns>
</DataGrid>
Thanks
I've found the perfect solution, based on an answer by xr280xr.
It works out of the box, in any condition, and without using additional code.
The style, that I put in <DataGrid.Resources> :
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type DataGridCell}">
<TextBlock Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content.Text}" TextTrimming="CharacterEllipsis">
<TextBlock.ToolTip>
<ToolTip Visibility="{Binding RelativeSource={RelativeSource Self}, Path=PlacementTarget, Converter={StaticResource TrimToVisConverter}}">
<ToolTip.Content>
<TextBlock Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content.Text}"/>
</ToolTip.Content>
</ToolTip>
</TextBlock.ToolTip>
</TextBlock>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The source of Converter={StaticResource TrimToVisConverter}:
public class TrimmedTextBlockVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value == null) return Visibility.Collapsed;
FrameworkElement textBlock = (FrameworkElement)value;
textBlock.Measure(new System.Windows.Size(Double.PositiveInfinity, Double.PositiveInfinity));
if (((FrameworkElement)value).ActualWidth < ((FrameworkElement)value).DesiredSize.Width)
return Visibility.Visible;
else
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
I can't comment yet, I lack points needed for it.
But I just want to add an extra tip to the accepted answer code, for noobs like me who want to use it.
If you want to link your Converter to its code, make sure to create a link in your Window/UserControl Resources as mentioned below:
<Window.Resources>
<local:TrimmedTextBlockVisibilityConverter x:Key="TrimToVisConverter" />
</Window.Resources>
Additional thought on accepted answer: You also have to rewrite any style that comes with default DataGridCell template as DataGridCell States. Otherwise you lose clean view on row select and other stuff...

Categories