Ленивая загрузка содержимого вкладки WPF

Приложение WPF организовано как TabControl с каждой вкладкой, содержащей другой экран.

Один элемент TabItem привязан к данным, которые требуется немного времени для загрузки. Поскольку этот TabItem представляет собой экран, который пользователи могут использовать редко, я бы хотел не загружать данные, пока пользователь не выберет вкладку.

Как я могу это сделать?

Ответ 1

Управление вкладками выполняется двумя способами,

  • Когда мы добавляем элементы Tab явно, каждый элемент табуляции загружается и инициализируется немедленно, содержащая все.
  • Когда мы привязываем ItemsSource к списку элементов, и мы устанавливаем разные шаблоны данных для каждого элемента данных, элемент управления табуляции будет создавать только одно представление "Контент" для выбранного элемента данных, и только когда выбран элемент табуляции, "Загружен", событие просмотра содержимого будет запущено, и содержимое будет загружено. И когда выбран другой элемент табуляции, событие "Разгруженное" будет запущено для ранее выбранного содержимого контента, а "Загруженный" будет запущен для нового выбранного элемента данных.

Использование второго метода немного сложно, но во время выполнения он, безусловно, уменьшит ресурсы, которые он использует, но во время переключения вкладок он может быть немного медленнее на время.

Вам необходимо создать пользовательский класс данных следующим образом

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

И вы должны создать список или массив TabItemData, и вы должны установить источник элементов TabControl в список/массив TabItemData.

Затем создайте ItemTemplate из TabControl как свойство привязки шаблона данных. Свойство "Заголовок".

Затем создайте в ContentTemplate TabControl шаблон данных, содержащий ContentControl, с ключом ContentTemplate ресурса, найденным в свойстве ResourceKey.

Ответ 2

Может быть, слишком поздно:) Но те, кто ищет ответ, могут попробовать это:

<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>

Также вы можете создать многоразовый стиль TabItem с использованием AttachedProperty, который будет содержать "отложенное" содержимое. Дайте мне знать, если это необходимо, я отредактирую ответ.

UPDATE:

Прикрепленное свойство:

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 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>

Пример:

<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>

Ответ 3

Как упоминалось в в @Tomas Levesque ответ на дубликат этого вопроса, простейшая вещь, которая будет работать, - отложить привязку значений, добавив уровень отторжения с помощью 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>

Тогда виртуальная машина просто должна иметь некоторую лень: -

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; }
}

И все готово.


В моем конкретном случае у меня были причины избегать этой конкретной схемы Xaml и мне нужно было определить мой DataTemplate в Resources. Это вызывает проблему, поскольку a DataTemplate может быть только x:Type d и, следовательно, Lazy<ModelA> не может быть выражен через это (и аннотации пользовательской разметки явно запрещены в таких определениях).

В этом случае наиболее простой способ состоит в том, чтобы определить минимальный производный конкретный тип: -

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; }
}

Используя вспомогательный помощник:

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

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

который затем может быть использован прямо через DataTemplate s: -

<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>

Можно сделать этот подход более общим, введя слабо типизированный 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(); } }
}

Это позволяет писать более компактный .NET-код:

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; }

По цене добавления слоя для сахарирования/депиляции:

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

И позволяет Xaml быть:

<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>

Ответ 4

Вы можете посмотреть событие SelectionChanged:

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

Это будет вызываться, когда выбранная вкладка будет изменена; в зависимости от того, созданы ли ваши вкладки посредством привязки к коллекции или нет (это лучше всего работает, если "нет" ), это может быть так же просто, как создать экземпляр UserControl, содержащий все элементы управления, которые вы хотите для страницы, затем добавив его к некоторому Panel (например, a Grid), который существует как заполнитель на этой вкладке.

Надеюсь, что это поможет!

Ответ 5

У меня была одна и та же проблема несколько дней назад, и это лучший подход, который я нашел до сих пор:

В многосвязном интерфейсе пользовательские элементы управления содержимым привязывались к данным в своих событиях Loaded. Это добавляет больше времени на общее время загрузки приложения. Затем я разделил привязку пользовательских элементов управления от загруженных событий к действию с более низким приоритетом через Dispatcher:

Dispatcher.BeginInvoke(new Action(() => { Bind(); }), DispatcherPriority.Background, null);