Open AtomCrafty opened 9 months ago
The setup here is strange. You should avoid storing DataTemplate
s in Resources
. I'm not sure what the designed behavior here is, but it fixed the issue for me locally. There's a separate place for DataTemplate
s:
<UserControl.DataTemplates>
<DataTemplate x:DataType="vm:TabViewModel">
Your TabControl
then goes to:
<TabControl Grid.Row="0" Grid.Column="1" ItemsSource="{Binding Tabs}" />
The lookup/creation will be automatic. One other issue, if you're using a ViewLocator, is that record types won't properly go through it (by default) as it can't derive from ViewModelBase
.
I deliberately did not declare the template in UserControl.DataTemplates
because that will screw up the tab headers if no ItemTemplate
is set. The issue I'm describing occurs either way, no matter whether the template is looked up from an ancestor's DataTemplates
list, specified explicitly as a static resource, or declared inline within a <TabControl.ContentTemplate>
tag.
The template lookup works without issue in all of these cases. What the TabControl
does with the template once it's resolved is what's causing problems. The example is not using a view locator and there is no reason for TabViewModel
to derive from ViewModelBase
since it has no mutable properties.
Just found this discussion about a similar issue. Or actually almost the exact opposite. Whereas I take issue with controls being reused when switching between tabs of the same data type, they had trouble because switching between tabs of different data types re-instantiated the templates every time. So basically the scenario I warned about in my "expected behavior" section.
Funnily enough, their solution would probably solve my issue as well. I'll still leave this open since I'm of the opinion that this should be the default behavior for tab controls in the majority of use cases. Materializing all tab contents ahead of time but only ever showing one of them would solve both of these issues. But ultimately that's not my call to make.
@AtomCrafty i haven't tried it yet but probably you could implement your own IDataTemplate and handle the creation to your needs. Im Avalonia.Samples you can find out how to do it.
You're half right. It's possible to force re-instantiation of the template by implementing a custom data template type that doesn't implement IRecyclingDataTemplate
, as that will prevent the content presenter from reusing the existing child here.
But that won't make tabs persistent, meaning that every time you switch to a tab, its controls will be recreated and scroll positions reset. That behavior is a direct consequence of how the TabControl
is implemented, since it simply does not preserve the controls for inactive tabs.
I've since switched to the solution from the linked discussion (discard TabControl
and instead use TabStrip
+ ItemsControl
with a Panel
container) and it works like a charm. I still feel like persistent tabs would be a more useful default behavior for the TabControl
, but I also understand if you close this as "by design."
I also hit a roadblock in trying to write an attached behavior for this. TabControl.SelectedContent
has an internal setter. So even if I could cache views by subscribing SelectionChanged
, I can't do anything with it. Should probably be documented that this scenario isn't possible if there's an intentional design limitation.
this sitution with recreating tab content can be treated as "by desing". But note there are same group of people comes from WinForms not only WPF. My opinion is this behavior should be easy cotiomizable simply becouse that menu avalonia users coms from winforms..
This s not my issue but maybe can be handled here. One of method to make state of tab content is to use onw cache in ViewLocator like this:
public class ViewLocator : IDataTemplate
{
private Dictionary<object, Control> _cache = new Dictionary<object, Control>();
public Control? Build(object? data)
{
if (data is null)
return null;
var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
var type = Type.GetType(name);
if (type != null)
{
if (_cache.TryGetValue(data, out var res))
{
return res;
}
var control = (Control)Activator.CreateInstance(type)!;
control.DataContext = data;
_cache[control.DataContext] = control;
return control;
}
return new TextBlock { Text = "Not Found: " + name };
}
public bool Match(object? data)
{
return data is ViewModelBase;
}
}
and it works - for example ListBox Vertical scrolling is preserved. But Data grid Vertical scroll IS NOT.. why ? Its data grid inner issue ? But.. ItemsControl + IsVisible sill works for both ListBox and Data grid..
<TabControl Margin="5"
ItemsSource="{Binding Tabs}">
<TabControl.ItemTemplate>
<DataTemplate x:DataType="vm:TabDataViewModel">
<TextBlock Text="{Binding HeaderText}" />
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
change tab and come back to first :
@KrzysztofDusko my best guess is that the data grid somehow resets its scroll position when it's attached or detached from one of the control trees. The IsVisible
solution doesn't have that issue since all controls are always attached, just not always visible.
Aside from that, I would not recommend the template caching approach in most cases, since the controls will never be deleted. Once a tab view model is removed from the bound collection, the corresponding control hierarchy should be properly freed, which will never happen since your view locator will keep them alive forever.
Describe the bug
A tab control with multiple tabs will only instantiate its content template a single time and re-use the same controls for all tabs. Only the
DataContext
is changed when switching between tabs. This means that control state like the position of a scroll viewer carries over from one tab to another, even though the scroll viewers in different tabs should be entirely independent.Imagine switching between documents in your IDE of choice and the scroll position is shared among all open files. I feel like this behavior is very unintuitive and I don't see a straightforward way to work around it without writing a completely custom tab control.
I haven't looked to deeply into this, but I assume this is what's happening: Contrary to most other
ItemsControl
s, theTabControl
only has a singleContentPresenter
, since only one item can be shown at a time. This however also means that when theDataContext
(a.k.a. the tab control'sSelectedContent
) changes to another object of the same type, said content presenter has no reason to create a second instance of the template.Expected behavior
Since the
TabControl
is anItemsControl
, I would expect each item to get its own instance of the appropriate data template. The obvious approach would be to force a new template instantiation whenever a new tab is selected, but I don't think that's a good solution either. Ideally, the control state within each tab should be independent, but persistent, so switching to another tab and back to the first should preserve the position of any scroll viewers on the first tab. To return to the IDE example: Imagine switching to another document and the scroll position is reset to the top every time.There might be a discussion about virtualization to be had here, but I'd posit that tab controls will - in most cases - contain a fairly small amount of tabs, so it should be possible to keep all of the template instances materialized at all times.
Screenshots
TabControl
on the right,ListBox
for comparison on the left. Three instances of the template are created in total.To Reproduce
Minimal example:
Environment