WPF标签页内容的懒加载

28

我的WPF应用程序以TabControl组织,每个选项卡包含不同的屏幕。

一个TabItem绑定到需要一些时间来加载的数据上。由于这个TabItem代表用户可能很少使用的屏幕,我希望在用户选择该选项卡之前不加载数据。

我该如何做?

7个回答

18

也许有点晚了 :) 但是那些正在寻找答案的人可以尝试这个:

<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样式,其中包含“延迟加载”的内容。如果需要,请告诉我,我将编辑答案。

附加属性:

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

标签项样式:

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

1
这是一个非常优秀的解决方案,因为即使不使用延迟加载内容,它也能保持普通TabItem的行为。它的表现完全符合预期。 - Thomas Luijken
2
+1 但是我在数据绑定方面遇到了一些问题,让我感到很困扰。请参考我的备选方案 - Ruben Bartelink

17

选项卡控件有两种方式:

  1. 当我们显式添加选项卡项时,每个选项卡项会被立即加载和初始化,包含全部内容。
  2. 当我们将ItemsSource绑定到项目列表,并为每个数据项设置不同的数据模板时,选项卡控件仅创建所选数据项的一个“内容”视图,只有在选中选项卡项时,“内容”视图的“Loaded”事件才会触发并加载内容。当选择不同的选项卡项时,“Unloaded”事件将为先前选定的内容视图触发,“Loaded”事件将为新选定的数据项触发。

使用第二种方法可能有点复杂,但运行时肯定会减少它所使用的资源,但在切换选项卡时,可能会稍微变慢一些。

你必须创建以下自定义数据类

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

你需要创建TabItemData的列表或数组,并将TabControl的项源设置为TabItemData的列表/数组。

然后,将TabControl的ItemTemplate创建为数据模板,绑定“Header”属性。

接着,将TabControl的ContentTemplate创建为包含ContentControl的数据模板,并使用ResourceKey属性中找到的Resource key的ContentTemplate。


3
如果你正在使用MVVM模式,自然会选择第二个选项。 - Kent Boogaart
使用第二个选项会导致标签在卸载时丢失状态吗? - yclevine
是的,但您可以使用MyBusinessObject中的属性来定义状态,该状态可以与控件的可视状态和任何其他逻辑状态同步。 - Akash Kava
听起来很不错,但是你如何将 ContentControl 与 ViewModel 中的资源键绑定在一起呢?我所有的尝试都失败了。 - Glaucus
这对我不起作用:<ContentControl ContentTemplate="{Binding ResourceKey}" /> - Glaucus
1
这个东西如果有概念证明就更有用了。它太难读了。 - Konrad

9

正如在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排列,并需要能够在Resources中定义我的DataTemplate。这会导致一个问题,因为一个DataTemplate只能被视为x:Type,因此不能通过它来表示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轻松地消耗它:

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

2
您可以查看 SelectionChanged 事件:

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

当所选标签页更改时,该事件将被调用;根据您的选项卡是否通过绑定到集合创建(如果“不是”,则此方法最有效),它可能只需要创建包含页面所需所有控件的 UserControl 实例,然后将其添加到某个 Panel(例如,Grid)中,该面板作为该选项卡上的占位符存在。
希望这能帮助到您!

0
一个快速简单的以Data为中心的解决方案是在选中IsSelected选项卡时通过样式设置DataContext
<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>

这里的 LazyProperty 是一个使用了一些惰性加载模式的属性,例如:

private MyVM _lazyProperty;
public MyVM LazyProperty => _lazyProperty ?? (_lazyProperty = new MyVM());

0

我几天前遇到了同样的问题,目前这是我找到的最佳解决方案:

在多标签界面中,内容用户控件绑定到它们的Loaded事件中的数据。这会增加整个应用程序加载时间。然后,我将用户控件的绑定从Loaded事件延迟到通过Dispatcher的较低优先级操作:

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

0
我发现了一个更简单的方法。只需等到选项卡被激活后再初始化ViewModel即可。
public int ActiveTab
{
    get
    {
        return _ActiveTab;
    }
    set
    {
        _ActiveTab = value;
        if (_ActiveTab == 3 && InventoryVM == null) InventoryVM = new InventoryVM();
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接