WPF UI persistence in TabControl - wpf

I am having issues with something that seems like it should be very simple but in fact has proven quite difficult.
Lets say you have a TabControl bound to an itemsource of ViewModels and the items displayed using a DataTemplate. Now lets say the DataTemplate consists of a Grid with two columns and a Grid splitter to resize the columns.
The problem is if you resize the columns on one tab, and switch to another tab, the columns are also resized. This is because the TabControl shares the DataTemplate among all tabs. This lack of UI persistence is applied to all elements of the template which can make for a frustrating experience when various UI components are adjusted. Another example is the scroll position in a DataGrid (on a tab). A DataGrid with few items will be scrolled out of view (only one row visible) if a DataGrid with more rows was scrolled to the bottom on another tab. On top of this, if the TabControl has various items defined in multiple DataTemplates the view is reset when you switch between items of differenet types. I can understand that this approach saves resources but the resultant functionality seems quite contradictory to expected UI behavior.
And so i'm wondering if there is a solution/workaround to this as i'm sure it's something that others have encountered before. I've noticed a few similar questions on other forums but there was no real solution. One about using the AdornerDecorator but that doesn't seem to work when used with a DataTemplate. I'm not keen on binding all the UI properties (like column width, scroll position) to my ViewModels and in fact I tried it for the simple GridSplitter example and I didn't manage to make it work. The width of the ColumnDefinitions were not necessarily affected by a grid splitter. Regardless, it would be nice if there were a general solution to this. Any thoughts?
If I ditch the TabControl and use an ItemsControl will I encounter a similar issue? Would it be possible to modify the TabControl Style so it doesn't share the ContentPresenter between tabs?

I've been messing with this on and off for a quite a while now. Finally, instead of trying to fix/modify the TabControl I simply recreated it's functionality. It's actually worked out really well. I made a Tab'like'Control out of a Listbox (Tab headers) and an ItemsControl. The key thing was to set the ItemsPanelTemplate of the ItemsControl to a Grid. A bit of Styling, and a DataTrigger to manage the Visibility of the Items and voila. It works perfect, each "Tab" is a unique object and preserves all it's UI states like scroll position, selections, column widths, etc. Any downsides or problems that might occur with this type of solution?
<DockPanel>
<ListBox
DockPanel.Dock="Top"
ItemsSource="{Binding Tabs}"
SelectedItem="{Binding SelectedTab}"
ItemContainerStyle="{StaticResource ImitateTabControlStyle}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel
Orientation="Horizontal">
</StackPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel
Margin="2,2,2,0"
Orientation="Horizontal" >
<TextBlock
Margin="4,0" FontWeight="Bold"
Padding="2"
VerticalAlignment="Center" HorizontalAlignment="Left"
Text="{Binding Name}" >
</TextBlock>
<Button
Margin="4,0"
Command="{Binding CloseCommand}">
<Image Source="/TERM;component/Images/Symbol-Delete.png" MaxHeight="20"/>
</Button>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ItemsControl
ItemsSource="{Binding Tabs}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl
Content="{Binding}">
<ContentControl.Style>
<Style>
<Style.Triggers>
<DataTrigger
Binding="{Binding IsSelected}" Value="False">
<Setter Property="ContentControl.Visibility" Value="Hidden" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DockPanel>

Related

ItemsControl Not Creating ContentPresenter

Why isn't my ItemsControl creating a ContentPresenter for each item? I'm guessing this is what's making my items not show up (they're set to visible and in the right spot when I inspect using the Live Visual Tree). I'm basically reusing code that works up above in a different ItemsControl and I haven't been able to find anything while searching Google/Stackoverflow with this issue. I can include view model code but I don't think it's related because I see the appropriate values in the Live Property Explorer and can see each WellContainer is in it's appropriate grid cell.
XAML:
<ItemsControl
Grid.Row="1"
Grid.Column="1"
ItemsSource="{Binding Wells}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid
x:Name="m_WellGrid"
Margin="5"
wpf:GridHelpers.RowCount="{Binding RowCount}"
wpf:GridHelpers.ColumnCount="{Binding ColumnCount}">
</Grid>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter
Property="Grid.Row"
Value="{Binding Path=WellRow}"/>
<Setter
Property="Grid.Column"
Value="{Binding Path=WellCol}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock
Text="A"
Margin="4"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Live Visual Tree Inspection:
The ItemsControl is designed to wrap the items in a container only when necessary, that is, when the item is not eligible to be its own container. From your comment we find that WellContainer derives from Control, thus is eligible to be its own container1 and is not wrapped in a ContentPresenter. Unfortunately there's no way to control this behavior directly, but you could subclass ItemsControl and override the ItemsControl.IsItemItsOwnContainerOverride method to modify the default behavior.
1 As we can see in the ItemsControl source code it is enough for the item to be of UIElement type to be eligible to be its own container.

Expander with Virtualization inside WPF Datagrid

I have a performance issue in a prototype I am working on. The requirement is to build a datagrid with multiple synchronized frozen panes, supporting grouping and sorting etc... For more details about the grid I am building, see this previous question.
Now, I have a question related to Grouping and in particular Expanders. I have a GroupStyle defined by the following Xaml, and taken from this blog post.
<!--Default GroupStyle-->
<GroupStyle x:Key="gs_Default">
<GroupStyle.Panel>
<ItemsPanelTemplate>
<DataGridRowsPresenter/>
</ItemsPanelTemplate>
</GroupStyle.Panel>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=Name}" FontWeight="Bold" Padding="3"/>
</StackPanel>
</DataTemplate>
</GroupStyle.HeaderTemplate>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Expander x:Name="exp"
BorderBrush="#FFA4B97F"
BorderThickness="0,0,0,1"
IsExpanded="{Binding Path=Items[0].IsExpanded}">
<Expander.Header>
<DockPanel TextBlock.FontWeight="Bold">
<TextBlock Text="{Binding Path=Name}" Margin="5,0,5,0" />
<TextBlock Text="{Binding Path=ItemCount}"/>
</DockPanel>
</Expander.Header>
<ItemsPresenter/>
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
The Expander is not virtualized and we are experiencing a performance issue when there are several hundred rows in a group.
Has anyone encoutered this before and have a fix? I am ideally looking for a Virtualizing Expander, and have seen workarounds such as this (which doesn't solve the problem).
If you're using .NET 4 then get the ItemsPresenters visibility to track/be dependent on the IsExpanded state.
WPF DataGrid Virtualization with Grouping
In .NET 4.5 there's a new attached property VirtualizingPanel.IsVirtualizingWhenGrouping.
Another alternative is to push the grouping into the ViewModel rather than making the DataGrid/CollectionView be responsible for that. See here:
http://blog.smoura.com/wpf-toolkit-datagrid-part-iv-templatecolumns-and-row-grouping/
In order to solve this, we did the following.
We used DataTemplates to present a different row view for different row viewmodels. We had a GroupRowViewModel and ItemRowViewModel. Also a parent ViewModel which had a sorted list of Group/Item viewmodels.
When the grid was instantiated, the parent ViewModel would sort all child viewmodels into the following:
Group
Item
Item
Item
Group
Item
Item
Item
When a GroupRow is clicked you want to execute some code where the parent (which contains a sorted list of group+item rows) will remove or include the items. E.g. say the second gropu was clicked, your list of rowviewmodels you bind to now becomes
Group
Item
Item
Item
Group (Collapsed)
That's it. So no magic at all, you manually remove or include the rows you want depending on what was clicked. It worked with virtualization and hundreds of thousands of rows at an acceptible speed.
Sorry I can't post any code (due to NDA) but I hope that helps you. Also - I would suggest looking at Telerik Grid as this is awesomely fast for large datasets

Scaling images in a WPF ListBox

I've started developing my code using the example from SO WPF: arranging collection items in a grid. Now, to gain cell selection capability, I renamed each ItemsControl to ListBox, because a ListBox is-a ItemsControl (XAMl somewhat simplified):
<ListBox HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" ItemsSource="{Binding YourItems}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Grid/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemContainerStyle>
<Style>
<Setter Property="Grid.Column" Value="{Binding X}"/>
<Setter Property="Grid.Row" Value="{Binding Y}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Image RenderOptions.BitmapScalingMode="LowQuality" Source="{Binding ...ImageSource, Mode=OneWay}">
</Image>
</DataTemplate>
</ListBox.ItemTemplate>
The grid is filled with glyph run test imaged, based on the code here.
Surprisingly it worked - kind of. Selection works. However, in the case of the ItemsControl, there was no scroll bar. Everything scaled nicely. When I made the window smaller, the grid cells shrunk and so did the images. When I made the window larger, everything scaled up.
Now, with the ListBox that's not the case. The images size stays fixed. If the window isn't large enough, there's an horizontal scroll bar and when the window isn't large enough, some of the images are hidden and the user needs to scroll to the right.
So, my question is: If a ListBox is-an ItemControl, why don't my images scale the same? What should I do to correct it?
This is because ListBox and ItemsControl use different styles. You might easily apply the ItemControl's default style to your ListBox:
<ListBox Style="{StaticResource ResourceKey={x:Type ItemsControl}}">

Getting UI virtualization working with ItemsControl in Silverlight

I'm trying to create a scrolling list of fairly large textblocks. I want there to be a vertical scrollbar to show them all, and if they overflow a certain size I want them to display an ellipsis. I actually have all this working pretty good.
I have the following Silverlight XAML:
<Grid x:Name="LayoutRoot" MaxWidth="500" MinWidth="100"
MaxHeight="500" MinHeight="100">
<Grid.DataContext>
<app:MainPageViewModel/>
</Grid.DataContext>
<ScrollViewer>
<ItemsControl ItemsSource="{Binding TextItems}" Margin="0,20,0,20">
<ItemsControl.ItemTemplate><DataTemplate>
<Border MaxHeight="175" Margin="0,0,0,18" CornerRadius="5">
<TextBlock Margin="2" TextTrimming="WordEllipsis"
TextWrapping="Wrap" Text="{Binding}"/>
</Border>
</DataTemplate></ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
My problem is that this layout does not use UI virtualization, such as with a VirtualizingStackPanel. So it is pretty slow. What is the best way to get UI virtualization into this layout? I've tried about a half dozen different ways and nothing has worked out all that well.
I managed to get this working in a ListBox because it seems to support virtualization out of the box. However, I'd prefer to use ItemsControl as I don't want these things to be selectable, and I don't want the styling that comes along with a ListBox.
This in Silverlight 4.
There are a few things you need to do to make this work.
Set the ItemsPanelTemplate for
your ItemsControl to a
VirtualizingStackPanel.
Incorporate the ScrollViewer inside
a ControlTemplate for your
ItemsControl instead of just
wrapping it around the outside.
Make sure the ItemsControl has a fixed height so the layout system can work out how many items it needs to fill the viewport. (It looks like you are already doing this by putting the ItemsControl in a Grid - that will allow the layout system to determine the alloted height for the control)
Here's the simplest example I could come up with of this working:
<ItemsControl ItemsSource="{Binding TextItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Template>
<ControlTemplate TargetType="ItemsControl">
<Border>
<ScrollViewer>
<ItemsPresenter/>
</ScrollViewer>
</Border>
</ControlTemplate>
</ItemsControl.Template>
</ItemsControl>

Silverlight 3 Checkbox Listbox bug when scrolling?

I've spent a few minutes searching on Google and have not found anything related to this issue I'm having:
Today I upgraded to the Silverlight 3 SDK and converted a project that I'm working on. I then noticed a bug in my program with a Listbox that has a Checkbox as its DataTemplate.
When one or more items is checked, and I scroll up and down, it seems that a few of the Checkboxes at the extremes get checked off and on randomly. This does not trigger the Checked/Unchecked event, however.
Has anyone seen this behavior? I'm not doing anything out of the ordinary, simply scrolling up and down once at least one checkbox has been checked, and a couple of others that I have not touched seem to get checked on and off repeatedly. This was definitely not happening with the Silverlight 2 SDK.
Here's the XAML definition for my Listbox:
<ListBox x:Name="cBoxSalesmen" Width="135" Height="200"
HorizontalAlignment="Left" VerticalAlignment="Top">
<ListBox.Template>
<ControlTemplate>
<Border Style="{StaticResource BorderStyleThin}">
<StackPanel Orientation="Vertical">
<TextBlock Text="Salesmen" />
<ScrollViewer Height="176" VerticalScrollBarVisibility="Visible" >
<ItemsPresenter />
</ScrollViewer>
</StackPanel>
</Border>
</ControlTemplate>
</ListBox.Template>
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox Margin="0" Content="{Binding}" FontSize="10" HorizontalAlignment="Left"
Checked="SalesmenCheckbox_Checked" Unchecked="SalesmenCheckbox_Unchecked"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The default ItemsPanel of the ListBox is the VirtualizingStackPanel. You can change it to use the StackPanel, this way you problem is solved.
Use this code:
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
<ListBox.ItemsPanel>
I suspect your problem is a result of ListBox (in SL3) now using an ItemCollectionGenerator. The concept behind this is that not all the objects found in the source data collection need to have had their corresponding instance of the DataTemplate created and added to the Visual Tree. As you scroll toward the bottom items that may soon be needed are created. Additionally items that have already be created but are now scrolled quite same way out of view can be removed. If the user scrolls up they are re-created.
If this is the case then the IsChecked state of any checkbox in this list will be lost at some point for large lists. To solve this you would need include a property in the data type to which you can bind IsChecked. Hence as ListBox re-creates items it correctly assigns the IsChecked value.

Resources