TabControl disposes of controls on inactive tabs - wpf

I'm using the MVVM pattern for my app. The MainWindow comprises a TabControl with the DataContext mapped to the ViewModel:
<Window.Resources>
<ResourceDictionary>
<DataTemplate x:Key="templateMainTabControl">
<ContentPresenter Content="{Binding Path=DisplayName}" />
</DataTemplate>
<local:ViewModel x:Key="VM" />
<local:WorkspaceSelector x:Key="WorkspaceSelector" />
<local:TabOneView x:Key="TabOneView" />
<local:TabTableView x:Key="TabTableView" />
<DataTemplate x:Key="TabOne">
<local:TabOneView />
</DataTemplate>
<DataTemplate x:Key="TabTable">
<local:TabTableView />
</DataTemplate>
</ResourceDictionary>
</Window.Resources>
<TabControl Grid.Row="0"
DataContext="{StaticResource VM}"
ItemsSource="{Binding Workspaces}"
SelectedItem="{Binding SelectedWorkspace}"
ItemTemplate="{StaticResource templateMainTabControl}"
ContentTemplateSelector="{StaticResource WorkspaceSelector}" />
The WorkspaceSelector looks like:
public class WorkspaceSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate( object item, DependencyObject container )
{
Window win = Application.Current.MainWindow;
Workspace w = ( Workspace ) item;
string key = w.DisplayName.Replace( " ", "" );
if ( key != "TabOne" )
{
key = "TabTable";
}
return win.FindResource( key ) as DataTemplate;
}
}
so that TabOne returns the DataTemplate. TabOne and the other two tabs return the DataTemplate TabTable.
If I run the application and click on each of the tabs twice (1, 2, 3, 1, 2, 3) I don't get what I expect, which is
TabOne's view is created
TabTwo's view is created
TabOne's View is created
TabTwo's view is created
that is, if the TemplateSelector returns a different value, the existing tab's controls are thrown away and the new tab's control's are created, and if the TemplateSelector returns the same value, nothing happens.
This is exactly what I don't want! I'd like the TabControl to keep all the controls on the tabs, and I would like to be able to do something about creating different controls in code for the case where I go from TabTwo to TabThree. I can live without the latter. But how do I tell the TabControl not to throw away each tab's controls when it's not selected?

This is a function of the TabControl and is the default behavior.
Basically, to save memory, the TabControl unloads the visual tree that is in its content area and replaces it with a newly crufted up one for the new tab. To prove this to yourself, you can listen to the Unload event on each control you template in and notice that it fires every time you switch tabs.
There are likely 2 reasons you would want to override this behavior:
You believe that there would be a significant performance penalty.
You are losing the state of the controls because any visual state that is being lost is not backed by a ViewModel.
As for #1, you shouldn't be concerned. CPU time is generally cheaper than RAM and the default behavior leans on the cheaper side of the resource equation. If you still feel like you REALLY don't want this behavior, you can see an example of overriding it here:
https://github.com/cefsharp/CefSharp/blob/master/CefSharp.Wpf.Example/Controls/NonReloadingTabControl.cs
However, I would consider this a "smell" for potentially a future performance issue you should spend the time figuring out now, rather than delaying figuring it out.
For #2, you have two options:
Make sure every property you want preserved (like IsSelected, etc) is backed by a ViewModel that preserves that state.
Create a persistent UserControl for each tab that you bind to, rather than to ViewModels (Workspaces in your case). There is an example of that in the "Writer" sample for WAF: http://waf.codeplex.com/

Related

How to have dynamically loaded instances of unbound named XAML elements behave independently from each other?

I have a WPF application where I dynamically load document view instances into a TabControl. The view has a ToolBar with some ToggleButtons which I use to control the visibility of certain elements in that view like so (only relevant elements shown):
<UserControl x:Class="MyProject.View.Views.DocumentView" ...>
...
<ToolBar>
<ToggleButton x:Name="togglePropInspector" ... />
...
</ToolBar>
...
<Border Visibility={Binding ElementName=togglePropInspector, Path=IsChecked, Converter={StaticResource BoolToVisibilityConverter}}">
...
</Border>
</UserControl>
I found this kinda neat as everything is handled inside the view and didn't require adding code to the view model (or code behind). However, the problem is that checking the toggle button on one tab now checks it in all instances of the view, not just the current tab. This basically applies to all elements whose state is not bound to the view model in any way. Is there a way around this without having to add code to the view model?
For completeness' sake here's the relevant part of how I'm loading the views:
<TabControl ItemsSource="{Binding Documents}">
<TabControl.Resources>
<DataTemplate DataType="{x:Type viewModels:DocumentViewModel}">
<local:DocumentView />
</DataTemplate>
</TabControl.Resources>
</TabControl>
TabControl has a single content host that is used for all TabItem instances. When the data models assigned to the TabItem.Content property are of the same data type, then the TabControl will reuse the same DataTemplate, which means same element instances, only updated with the changed data from data bindings.
To change the state of the reused controls, you must either access the control explicitly, or force the TabControl to reapply the ContentTemplate by temporarily changing the data type of the Content:
<TabControl SelectionChanged="TabControl_SelectionChanged" />
private void TabControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var tabControl = sender as TabControl;
var tabItemContainer = tabControl.ItemContainerGenerator.ContainerFromItem(tabControl.SelectedItem) as TabItem;
object currentContent = tabItemContainer.Content;
tabItemContainer.Content = null;
// Defer and leave the context to allow the TabControl to handle the new data type (null).
// The content switch shouldn't be noticable in the GUI.
Dispatcher.InvokeAsync(() => tabItemContainer.Content = currentContent);
}
You can also use a dedicated data type for each tab. This way the TabControl is automatically forced to switch the DataTemplate.
The cleanest solution would be to bind the ToggleButton to the data model.

Showing UserControl once at the time with DataTemplateSelector

I have a couple specific user controls to Show some Content, e.g. simple like Image, WebControl but also two complex specific custom controls drawing on a canvas.
Now I thought using the DataTemplateSelector to handle the different UserControls. I actully used this http://tech.pro/tutorial/807/wpf-tutorial-how-to-use-a-datatemplateselector as a reference.
I changed the code so the form loads the UserControls dynamically (according to the file extension) in the following collection:
ObservableCollection<string> _pathCollection = new ObservableCollection<string>();
The only difference to the reference is now I want to navigate back and forward to the next control by showing one control only at the time. Which control should I use instead of ListView?
<Grid>
<ListView ScrollViewer.CanContentScroll="False"
ItemsSource="{Binding ElementName=This, Path=PathCollection}"
ItemTemplateSelector="{StaticResource imgStringTemplateSelector}">
</ListView>
</Grid>
How do I need to bind it to the template (equal to ItemTemplateSelector above)? WPF is still very new to me and I am learning.
Use a ContentControl. Bind your current item to the Content-property and the DataTemplateSelector to the ContentTemplateSelector-property.
<ContentControl Content="{Binding Path=CurrentItem, Mode=OneWay}", ContentTemplateSelector="{StaticResource imgStringTemplateSelector}" />
Your CurrentItem should be a DependencyProperty or a INotifyPropertyChanged-property of your DataContext. When you change your CurrentItem, the ContentControl will update the template automatically with help of your TemplateSelector.

Lifetime for the view / viewmodel when using data templates

I am defining a strategy where a main view will use data templates to switch between the views. Currently it can switch between 3 Views:
ApplicationView: it is actually the view that consists of lots of
different views, mostly layered out using tabs / docking. this is a
view that deals with application data.
LogInView: it is used for logging the user in.
DialogView: it is used to display dialog views. This view will also use data templates to select a proper view that is required.
The idea is that when a dialog view needs to be displayed, it is set as current view on the main view. After the selection is done, this information is passed to ApplicationView, or a view that is part of ApplicationView. While DialogView is shown, ApplicationView, must not be released from memory, since it ApplicationViewModel will still be manipulating with data (it needs to constantly work in the background).
I am thinking of achieving this using DataTemplates, and binding ContentControl's Content to CurrentView:
// in MainView
DataTemplate DataType="{x:Type vm:ApplicationViewModel}">
<vw:ApplicationView />
</DataTemplate>
.....
// in MainViewModel
public ViewModelBase CurrentView { get; set; }
Basically I am trying to avoid using modal windows for dialogs.
1) Is this strategy OK, or there are some problems that I am not aware with it?
2) When I switch to DialogView (I am actually switching viewmodels), what happens with the ApplicationView/ApplicationViewModel? Do I need to store ApplicationViewModel's reference somewhere, so it doesn't get garbage collected? I haven't tested this, but probably when I set CurrentView a new instance of ViewModel/View will be created.
3) Connected to second question, when using DataTemplates, what happens to View/ ViewModel that was previously used, and is now replaced with different view/viewmodel?
I don't see anything wrong with the way you're switching views, although typically you don't want to get rid of the application when you're displaying a dialog.
What I've done in the past is to put both the CurrentView, and the DialogView in a Grid so they are positioned on top of each other, then have the ApplicationViewModel contain an IDialogViewModel and IsDialogVisible properties, and when you want to display the dialog simply populate those two fields. (see below for an example)
You will have to store the ApplicationViewModel somewhere if you want to go back to it and avoid creating a new ApplicationViewModel
WPF disposes of UI objects that are no longer visible, so switching the CurrentView from Login to Application will get rid of the LoginView and create an ApplicationView
The ContentControl's Content is getting set to your ViewModel, so the ViewModel is actually being put in the applications VisualTree. Whenever WPF encounters an object in its VisualTree that it doesn't know how to draw, it will draw it using a TextBlock containing the .ToString() of the the object. By defining a DataTemplate, you are telling WPF how to draw the object instead of using its default .ToString() method. Once the object leaves the VisualTree, any visual objects that were created to render the object will get destroyed.
Although I would keep using what you currently have for switching Views, I would not use that method for the Login, Application, and Dialog views.
Typically the LoginView should only be displayed once when logging in, although it might get displayed again in a Dialog if you allow users to switch logins once logging in. Because of this, I typically show the LoginView in the startup code, then display the ApplicationView once login is successful.
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var login = new LoginDialog();
var loginVm = new LoginViewModel();
login.DataContext = loginVm;
login.ShowDialog();
if (!login.DialogResult.GetValueOrDefault())
{
Environment.Exit(0);
}
// Providing we have a successful login, startup application
var app = new ApplicationView();
var context = new ApplicationViewModel(loginVm.CurrentUser);
app.DataContext = context;
app.Show();
}
Like I said earlier, I wouldn't want to hide the Application when I display a Dialog, so I would make the Dialog part of the Application
Here's an example of the DataTemplate I would use for my ApplicationViewModel, using my own custom Popup from my blog for the Dialog
<Grid x:Name="ApplicationView">
<ContentControl Content="{Binding CurrentView}" />
<local:PopupPanel x:Name="DialogPopup"
Content="{Binding DialogContent}"
local:PopupPanel.IsPopupVisible="{Binding IsDialogVisible}"
local:PopupPanel.PopupParent="{Binding ElementName=ApplicationView}" />
</Grid>
Personally, I would find it easier to use ZOrdering within a standard grid, and put everything in the same view - using the ViewModel to manage visibility.
E.g
<Grid>
<Grid Visibility="{Binding IsView1Visible,
Converter={StaticResource BoolToVisibilityConverter}}">
<!-- view 1 contents -->
</Grid>
<Grid Visibility="{Binding IsView2Visible,
Converter={StaticResource BoolToVisibilityConverter}}">
<!-- view 2 contents -->
</Grid>
<Grid Visibility="{Binding IsView3Visible,
Converter={StaticResource BoolToVisibilityConverter}}">
<!-- view 3 contents -->
</Grid>
<Grid Visibility="{Binding IsDialogVisible,
Converter={StaticResource BoolToVisibilityConverter}}">
<!-- dialog contents contents -->
</Grid>
</Grid>

TabControl's SelectedItem gets overwritten by NewItemPlaceholder when adding tab

I'm working on a WPF TabControl whose last item is always a button to add a new tab, similar to Firefox:
The TabControl's ItemSource is bound to an ObservableCollection, and adding an item to the collection via this "+" button works very well. The only problem I'm having is that, after having clicked the "+" tab, I cannot for the life of me set the newly created (or any other existing tab) to focus, and so when a tab is added, the UI looks like this:
To explain a bit how I'm achieving this "special" tab behavior, the TabControl is templated and its NewButtonHeaderTemplate has a control (Image in my case) which calls the AddListener Command in the view-model (only relevant code is shown):
<Window x:Class="AIS2.PortListener.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ais="http://www.leica-geosystems.com/xaml"
xmlns:l="clr-namespace:AIS2.PortListener"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WPF4"
DataContext="{Binding Source={StaticResource Locator}>
<Window.Resources>
<ResourceDictionary>
<DataTemplate x:Key="newTabButtonHeaderTemplate">
<Grid>
<Image Source="..\Images\add.png" Height="16" Width="16">
</Image>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonDown">
<cmd:EventToCommand
Command="{Binding Source={StaticResource Locator},
Path=PortListenerVM.AddListenerCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Grid>
</DataTemplate>
<DataTemplate x:Key="newTabButtonContentTemplate"/>
<DataTemplate x:Key="itemHeaderTemplate">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
<DataTemplate x:Key="itemContentTemplate">
<l:ListenerControl></l:ListenerControl>
</DataTemplate>
<l:ItemHeaderTemplateSelector x:Key="headerTemplateSelector"
NewButtonHeaderTemplate="{StaticResource newTabButtonHeaderTemplate}"
ItemHeaderTemplate="{StaticResource itemHeaderTemplate}"/>
<l:ItemContentTemplateSelector x:Key="contentTemplateSelector"
NewButtonContentTemplate="{StaticResource newTabButtonContentTemplate}"
ItemContentTemplate="{StaticResource itemContentTemplate}"/>
</ResourceDictionary>
</Window.Resources>
<TabControl Name="MainTab" Grid.Row="2" ItemsSource="{Binding Listeners}"
ItemTemplateSelector="{StaticResource headerTemplateSelector}"
ContentTemplateSelector="{StaticResource contentTemplateSelector}"
SelectedItem="{Binding SelectedListener}">
</TabControl>
The AddListener command simply adds an item to the ObservableCollection which has for effect to update the TabControl's ItemSource and add a new tab:
private ObservableCollection<Listener> _Listeners;
public ObservableCollection<Listener> Listeners
{
get { return _Listeners; }
}
private object _SelectedListener;
public object SelectedListener
{
get { return _SelectedListener; }
set
{
_SelectedListener = value;
OnPropertyChanged("SelectedListener");
}
}
public PortListenerViewModel()
{
// Place the "+" tab at the end of the tab control
var itemsView = (IEditableCollectionView)CollectionViewSource.GetDefaultView(_Listeners);
itemsView.NewItemPlaceholderPosition = NewItemPlaceholderPosition.AtEnd;
}
private RelayCommand _AddListenerCommand;
public RelayCommand AddListenerCommand
{
get
{
if (_AddListenerCommand == null)
_AddListenerCommand = new RelayCommand(param => this.AddListener());
return _AddListenerCommand;
}
}
public void AddListener()
{
var newListener = new TCPListener(0, "New listener");
this.Listeners.Add(newListener);
// The following two lines update the property, but the focus does not change
//this.SelectedListener = newListener;
//this.SelectedListener = this.Listeners[0];
}
But setting the SelectedListener property does not work, even though the TabControl's SelectedItem is bound to it. It must have something to do with the order in which things get updated in WPF, because if I set a breakpoint in the SelectedListener's set I can see the following happening:
this.Listeners.Add(newListener);
this.SelectedListener = newListener;
SelectedListener set gets called with correct Listener object
SelectedListener set gets called with NewItemPlaceholder object (of type MS.Internal.NamedObject according to the debugger)
Is there a way that I can work around this issue? Do I have the wrong approach?
I think you are triggering two events when you click the new tab: MouseLeftButtonDown and TabControl.SelectionChanged
I think they're both getting queued, then processing one at a time.
So your item is getting added, set as selected, and then before the re-draw occurs the SelectionChanged event occurs to change the selection to the [+] tab.
Perhaps try using the Dispatcher to set the SelectedItem so it occurs after the TabControl changes it's selection. Or make it so if the user tries to switch to the NewTab, it cancels the SelectionChanged event so the selected tab doesn't actually change (of course, the SelectedTab will be your NewItem since the MouseDown event will have occurred)
When I did something like this in the past, I actually overwrote the TabControl Template to create the AddTab button as a Button, not as a TabItem. I want to suggest doing that instead of using the NewItemPlaceholder in the first place, but I've never tried working with the NewItemPlaceholder so don't really know if it's better or worse than overwriting the Template.
Take a look at this post regarding sentinel objects: WPF Sentinel objects and how to check for an internal type
There are several ways to work around issues with them, that post offers one of them.

How do you navigate a complex Visual Tree in order to re-bind an existing element?

In the above image, child is a ContentPresenter. Its Content is a ViewModel. However, its ContentTemplate is null.
In my XAML, I have a TabControl with the following structure:
<local:SuperTabControlEx DataContext="{Binding WorkSpaceListViewModel}"
x:Name="superTabControl1" CloseButtonVisibility="Visible" TabStyle="OneNote2007" ClipToBounds="False" ContentInnerBorderBrush="Red" FontSize="24" >
<local:SuperTabControlEx.ItemsSource>
<Binding Path="WorkSpaceViewModels" />
</local:SuperTabControlEx.ItemsSource>
<TabControl.Template>
<ControlTemplate
TargetType="TabControl">
<DockPanel>
<TabPanel
DockPanel.Dock="Top"
IsItemsHost="True" />
<Grid
DockPanel.Dock="Bottom"
x:Name="PART_ItemsHolder" />
</DockPanel>
<!-- no content presenter -->
</ControlTemplate>
</TabControl.Template>
<TabControl.Resources>
<DataTemplate DataType="{x:Type vm:WorkSpaceViewModel}">
....
WorkSpaceViewModels is an ObservableCollection of WorkSpaceViewModel. This code uses the code and technique from Keeping the WPF Tab Control from destroying its children.
The correct DataTemplate - shown above in the TabControl.Resource - appears to be rendering my ViewModel for two Tabs.
However, my basic question is, how is my view getting hooked up to my WorkSpaceViewModel, yet, the ContentTemplate on the ContentPresenter is null? My requirement is to access a visual component from the ViewModel because a setting for the view is becoming unbound from its property in the ViewModel upon certain user actions, and I need to rebind it.
The DataTemplate is "implicitly" defined. The ContentPresenter will first use it's ContentTemplate/Selector, if any is defined. If not, then it will search for a DataTemplate resource without an explicit x:Key and whose DataType matches the type of it's Content.
This is discussed here and here.
The View Model shouldn't really know about it's associated View. It sounds like there is something wrong with your Bindings, as in general you should not have to "rebind" them. Either way, an attached behavior would be a good way to accomplish that.
I think the full answer to this question entails DrWPF's full series ItemsControl: A to Z. However, I believe the gist lies in where the visual elements get stored when a DataTemplate is "inflated" to display the data item it has been linked to by the framework.
In the section Introduction to Control Templates of "ItemsControl: 'L' is for Lookless", DrWPF explains that "We’ve already learned that a DataTemplate is used to declare the visual representation of a data item that appears within an application’s logical tree. In ‘P’ is for Panel, we learned that an ItemsPanelTemplate is used to declare the items host used within an ItemsControl."
For my issue, I still have not successfully navigated the visual tree in order to get a reference to my splitter item. This is my best attempt so far:
// w1 is a Window
SuperTabControlEx stc = w1.FindName("superTabControl1") as SuperTabControlEx;
//SuperTabItem sti = (SuperTabItem)(stc.ItemContainerGenerator.ContainerFromItem(stc.Items.CurrentItem));
ContentPresenter myContentPresenter = FindVisualChild<ContentPresenter>(stc);
//ContentPresenter myContentPresenter = FindVisualChild<ContentPresenter>(sti);
DataTemplate myDataTemplate = myContentPresenter.ContentTemplate;
The above code is an attempt to implement the techniques shown on the msdn web site. However, when I apply it to my code, everything looks good, except myDataTemplate comes back null. As you can see, I attempted the same technique on SuperTabControlEx and SuperTabItem, derived from TabControl and TabItem, respectively. As described in my original post, and evident in the XAML snippet, the SuperTabControlEx also implements code from Keeping the WPF Tab Control from destroying its children.
At this point, perhaps more than anything else, I think this is an exercise in navigating the Visual Tree. I am going to modify the title of the question to reflect my new conceptions of the issue.

Resources