Lazy loading WPF tab content
Asked Answered
R

7

28

My WPF application is organized as a TabControl with each tab containing a different screen.

One TabItem is bound to data that takes a little while to load. Since this TabItem represents a screen that users may only rarely use, I would like to not load the data until the user selects the tab.

How can I do this?

Rahmann answered 18/7, 2010 at 6:57 Comment(0)
S
17

Tab control works two ways,

  1. When we add Tab Items explicitly, each tab item is loaded and initialized immediately containing every thing.
  2. When we bind ItemsSource to list of items, and we set different data template for each data item, tab control will create only one "Content" view of selected data item, and only when the tab item is selected, "Loaded" event of content view will be fired and content will be loaded. And when different tab item is selected, "Unloaded" event will be fired for previously selected content view and "Loaded" will be fired for new selected data item.

Using 2nd method is little complicated, but at runtime it will certainly reduce the resources it is using, but at time of switching tabs, it may be little slower for a while.

You have to create custom data class as following

class TabItemData{
   public string Header {get;set;}
   public string ResourceKey {get;set;}
   public object MyBusinessObject {get;set;}
}

And you must create list or array of TabItemData and you must set TabControl's items source to list/array of TabItemData.

Then create ItemTemplate of TabControl as data template binding "Header" property.

Then create create ContentTemplate of TabControl as data template containing ContentControl with ContentTemplate of Resource key found in ResourceKey property.

Speechmaker answered 18/7, 2010 at 8:5 Comment(6)
+1 You will use the second option naturally if you're doing MVVM.Radiant
Will using the second option cause the tabs to lose state when they are unloaded?Rahmann
Yes, but you can use properties in MyBusinessObject to define state that can be synchronized with visual state and any other logical state of control.Speechmaker
Sounds great, but how do you bind the ContentControl to the resource key from the viewmodel? All my attempts have failed.Fruitarian
This does not work for me: <ContentControl ContentTemplate="{Binding ResourceKey}" />Fruitarian
This would be more useful with proof of concept. It's too hard to read.Sells
P
18

May be too late :) But those who looking for an answer could try this:

<TabItem>
    <TabItem.Style>
        <Style TargetType="TabItem">
            <Style.Triggers>
                <Trigger Property="IsSelected" Value="True">
                    <Setter Property="Content">
                        <Setter.Value>
                            <!-- Your tab item content -->
                        </Setter.Value>
                    </Setter>
                </Trigger>
                <Trigger Property="IsSelected" Value="False">
                    <Setter Property="Content" Value="{Binding Content, RelativeSource={RelativeSource Self}}"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </TabItem.Style>  
</TabItem>

Also you can create a reusable TabItem style with using of AttachedProperty that will contain a "deferred" content. Let me know if this needed, I will edit answer.

Attached property:

public class Deferred
{
    public static readonly DependencyProperty ContentProperty =
        DependencyProperty.RegisterAttached(
            "Content",
            typeof(object),
            typeof(Deferred),
            new PropertyMetadata());

    public static object GetContent(DependencyObject obj)
    {
        return obj.GetValue(ContentProperty);
    }

    public static void SetContent(DependencyObject obj, object value)
    {
        obj.SetValue(ContentProperty, value);
    }
}

TabItem style:

<Style TargetType="TabItem">
    <Style.Triggers>
        <Trigger Property="IsSelected" Value="True">
            <Setter Property="Content" Value="{Binding Path=(namespace:Deferred.Content), RelativeSource={RelativeSource Self}}"/>
        </Trigger>
        <Trigger Property="IsSelected" Value="False">
            <Setter Property="Content" Value="{Binding Content, RelativeSource={RelativeSource Self}}"/>
        </Trigger>
    </Style.Triggers>
</Style>

Example:

<TabControl>
    <TabItem Header="TabItem1">
        <namespace:Deferred.Content>
            <TextBlock>
                DeferredContent1
            </TextBlock>
        </namespace:Deferred.Content>
    </TabItem>
    <TabItem Header="TabItem2">
        <namespace:Deferred.Content>
            <TextBlock>
                DeferredContent2
            </TextBlock>
        </namespace:Deferred.Content>
    </TabItem>
</TabControl>
Preponderant answered 19/9, 2012 at 15:59 Comment(2)
Excellent solution because this also keeps the normal tabitem behaviour intact when not using deferred content. This works exactly as expected.Cyclohexane
+1 but I tied myself in knots with it as the data binding can break. See my alternate solutionSwamy
S
17

Tab control works two ways,

  1. When we add Tab Items explicitly, each tab item is loaded and initialized immediately containing every thing.
  2. When we bind ItemsSource to list of items, and we set different data template for each data item, tab control will create only one "Content" view of selected data item, and only when the tab item is selected, "Loaded" event of content view will be fired and content will be loaded. And when different tab item is selected, "Unloaded" event will be fired for previously selected content view and "Loaded" will be fired for new selected data item.

Using 2nd method is little complicated, but at runtime it will certainly reduce the resources it is using, but at time of switching tabs, it may be little slower for a while.

You have to create custom data class as following

class TabItemData{
   public string Header {get;set;}
   public string ResourceKey {get;set;}
   public object MyBusinessObject {get;set;}
}

And you must create list or array of TabItemData and you must set TabControl's items source to list/array of TabItemData.

Then create ItemTemplate of TabControl as data template binding "Header" property.

Then create create ContentTemplate of TabControl as data template containing ContentControl with ContentTemplate of Resource key found in ResourceKey property.

Speechmaker answered 18/7, 2010 at 8:5 Comment(6)
+1 You will use the second option naturally if you're doing MVVM.Radiant
Will using the second option cause the tabs to lose state when they are unloaded?Rahmann
Yes, but you can use properties in MyBusinessObject to define state that can be synchronized with visual state and any other logical state of control.Speechmaker
Sounds great, but how do you bind the ContentControl to the resource key from the viewmodel? All my attempts have failed.Fruitarian
This does not work for me: <ContentControl ContentTemplate="{Binding ResourceKey}" />Fruitarian
This would be more useful with proof of concept. It's too hard to read.Sells
S
9

As alluded to in @Tomas Levesque's answer to a duplicate of this question, the simplest thing that will work is to defer the binding of the values by adding a level of inditection via a ContentTemplate DataTemplate:-

<TabControl>
    <TabItem Header="A" Content="{Binding A}">
        <TabItem.ContentTemplate>
            <DataTemplate>
                <local:AView DataContext="{Binding Value}" />
            </DataTemplate>
        </TabItem.ContentTemplate>
    </TabItem>
    <TabItem Header="B" Content="{Binding B}">
        <TabItem.ContentTemplate>
            <DataTemplate>
                <local:BView DataContext="{Binding Value}" />
            </DataTemplate>
        </TabItem.ContentTemplate>
    </TabItem>
</TabControl>

Then the VM just needs to have some laziness:-

public class PageModel
{
    public PageModel()
    {
        A = new Lazy<ModelA>(() => new ModelA());
        B = new Lazy<ModelB>(() => new ModelB());
    }

    public Lazy<ModelA> A { get; private set; }
    public Lazy<ModelB> B { get; private set; }
}

And you're done.


In my particular case, I had reason to avoid that particular Xaml arrangement and needed to be able to define my DataTemplates in the Resources. This causes a problem as a DataTemplate can only be x:Typed and hence Lazy<ModelA> can not be expressed via that (and custom markup annotations are explicitly forbidden in such definitions).

In that case, the most straightforward route around that is to define a minimal derived concrete type:-

public class PageModel
{
    public PageModel()
    {
        A = new LazyModelA(() => new ModelA());
        B = new LazyModelB(() => new ModelB());
    }

    public LazyModelA A { get; private set; }
    public LazyModelB B { get; private set; }
}

Using a helper like so:

public class LazyModelA : Lazy<ModelA>
{
    public LazyModelA(Func<ModelA> factory) : base(factory)
    {
    }
}

public class LazyModelB : Lazy<ModelB>
{
    public LazyModelB(Func<ModelB> factory) : base(factory)
    {
    }
}

Which can then be consumed straightforwardly via DataTemplates:-

<UserControl.Resources>
    <DataTemplate DataType="{x:Type local:LazyModelA}">
        <local:ViewA DataContext="{Binding Value}" />
    </DataTemplate>
    <DataTemplate DataType="{x:Type local:LazyModelB}">
        <local:ViewB DataContext="{Binding Value}" />
    </DataTemplate>
</UserControl.Resources>
<TabControl>
    <TabItem Header="A" Content="{Binding A}"/>
    <TabItem Header="B" Content="{Binding B}"/>
</TabControl>

One can make that approach more generic by introducing a loosely typed ViewModel:

public class LazyModel
{
    public static LazyModel Create<T>(Lazy<T> inner)
    {
        return new LazyModel { _get = () => inner.Value };
    }

    Func<object> _get;

    LazyModel(Func<object> get)
    {
        _get = get;
    }

    public object Value { get { return _get(); } }
}

This allows you to write more compact .NET code:

public class PageModel
{
    public PageModel()
    {
        A = new Lazy<ModelA>(() => new ModelA());
        B = new Lazy<ModelB>(() => new ModelB());
    }

    public Lazy<ModelA> A { get; private set; }
    public Lazy<ModelB> B { get; private set; }

At the price of adding a sugaring/detyping layer:

    // Ideal for sticking in a #region :)
    public LazyModel AXaml { get { return LazyModel.Create(A); } }
    public LazyModel BXaml { get { return LazyModel.Create(B); } }

And allows the Xaml to be:

<UserControl.Resources>
    <DataTemplate DataType="{x:Type local:ModelA}">
        <local:ViewA />
    </DataTemplate>
    <DataTemplate DataType="{x:Type local:ModelB}">
        <local:ViewB />
    </DataTemplate>
    <DataTemplate DataType="{x:Type local:LazyModel}">
        <ContentPresenter Content="{Binding Value}" />
    </DataTemplate>
</UserControl.Resources>
<TabControl>
    <TabItem Header="A" Content="{Binding AXaml}" />
    <TabItem Header="B" Content="{Binding BXaml}" />
</TabControl>
Swamy answered 20/11, 2015 at 13:4 Comment(0)
F
2

You could look at the SelectionChanged event:

http://msdn.microsoft.com/en-us/library/system.windows.controls.primitives.selector.selectionchanged.aspx

That will be called when the selected tab is changed; depending on whether your tabs are created through a binding to a collection or not (this works best if 'not'), it could be as simple as creating an instance of a UserControl containing all the controls you want for the page, then adding it to some Panel (for example, a Grid) that exists as a placeholder on that tab.

Hope that helps!

Friedly answered 18/7, 2010 at 7:51 Comment(0)
K
0

I have been thru same problem few days back and this is the best approach I found so far:

In a multitabbed interface, the content user controls were bound to data in their Loaded events. This adding more time to the overall application load time. Then I differed the binding of user controls from Loaded events to a lower priority action via Dispatcher:

Dispatcher.BeginInvoke(new Action(() => { Bind(); }), DispatcherPriority.Background, null);
Kipkipling answered 18/7, 2010 at 6:57 Comment(0)
K
0

I found a much simpler way. Simply wait to initialize the ViewModel until the tab is activated.

public int ActiveTab
{
    get
    {
        return _ActiveTab;
    }
    set
    {
        _ActiveTab = value;
        if (_ActiveTab == 3 && InventoryVM == null) InventoryVM = new InventoryVM();
    }
}
Keratin answered 9/12, 2018 at 2:34 Comment(0)
C
0

A quick and simple Data-centric solution would be to set DataContext by style when the tab IsSelected

<Style TargetType="{x:Type TabItem}">
    <Setter Property="DataContext" Value="{x:Null}"/> <!--unset previous dc-->
    <Style.Triggers>
        <Trigger Property="IsSelected" Value="True">
            <Setter Property="DataContext" Value="{Binding LazyProperty}"/>
        </Trigger>
    </Style.Triggers>
</Style>

where LazyProperty is a property using some of the lazy load patterns, for example:

private MyVM _lazyProperty;
public MyVM LazyProperty => _lazyProperty ?? (_lazyProperty = new MyVM());
Coating answered 3/4, 2020 at 9:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.