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?

yclevine
  • 990
  • 3
  • 9
  • 20

7 Answers7

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>
StayOnTarget
  • 11,743
  • 10
  • 52
  • 81
  • 1
    Excellent solution because this also keeps the normal tabitem behaviour intact when not using deferred content. This works exactly as expected. – Thomas Luijken Aug 25 '15 at 08:46
  • 2
    +1 but I tied myself in knots with it as the data binding can break. [See my alternate solution](http://stackoverflow.com/a/33827448/11635) – Ruben Bartelink Nov 20 '15 at 13:06
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.

Akash Kava
  • 39,066
  • 20
  • 121
  • 167
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>
Community
  • 1
  • 1
Ruben Bartelink
  • 59,778
  • 26
  • 187
  • 249
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!

Kieren Johnstone
  • 41,277
  • 16
  • 94
  • 144
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);
Arpit Khandelwal
  • 1,743
  • 3
  • 23
  • 34
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();
    }
}
Neil B
  • 2,096
  • 1
  • 12
  • 23
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());
wondra
  • 3,271
  • 3
  • 29
  • 48