Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lazy loading WPF tab content

Tags:

.net

wpf

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?

like image 972
yclevine Avatar asked Jul 18 '10 06:07

yclevine


Video Answer


4 Answers

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.

like image 133
Akash Kava Avatar answered Oct 28 '22 12:10

Akash Kava


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>
like image 17
Danylo Yelizarov Avatar answered Oct 28 '22 13:10

Danylo Yelizarov


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>
like image 9
Ruben Bartelink Avatar answered Oct 28 '22 12:10

Ruben Bartelink


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!

like image 2
Kieren Johnstone Avatar answered Oct 28 '22 13:10

Kieren Johnstone