使用代码隐藏布局更改的 TabControl 缓存
TabControl caching with code behind layout changes
关于这个 SO 问题 here,是否可以从后面的代码更新其他选项卡内容并让缓存允许重新缓存已更改的 UI 元素?就像在场景中我已经更新了 DataGrid
某些事件 TabControl
上 tabs
的滚动索引,
dgvLogs.ScrollIntoView( log );
现在,由于选项卡已被缓存,并且当用户切换到 dgvLogs
所在的选项卡时,上面的更改不会反映出来。
编辑
我在主 window 中有选项卡控件 (ExTabControl
) 和多个包含 datagrid
的选项卡,其中显示了一些应用程序日志。像这样:
ExTabControl
像这样:
<controls:ExTabControl Grid.Row="1" ItemsSource="{Binding Tabs, Mode=OneWay}" >
<controls:ExTabControl.Resources>
<Style TargetType="{x:Type TabItem}">
</Style>
</controls:ExTabControl.Resources>
</controls:ExTabControl>
单个选项卡具有这样的数据网格:
<DataGrid Name="dgvLogs" ItemsSource="{Binding Logs}" VerticalScrollBarVisibility="Auto" FrozenColumnCount="4">
问题:
假设我在 ExTabControl
中有 3 个选项卡,selected 选项卡是 1,后面的代码使用 dgvLogs.ScrollIntoView( someInbetweenlog );
更新了选项卡 2 的滚动索引。理想情况下,如果我执行 select 选项卡 2,那么 dgvLogs
内的 select 滚动索引应该位于 someInbetweenlog
所在的位置。但不幸的是,标签 2 滚动不会根据更改后的代码移动..
如果我确实使用默认选项卡控件,即 TabControl
代替 ExTabControl
,那么它会按预期正常工作。但是如果我在 dgvLogs
的任何选项卡中移动滚动,那么它也会反映在其他选项卡中..
请添加评论,如果需要,我会 post 更多代码。
编辑 2
我创建了示例应用程序,试图在其中演示该问题。在这个应用程序中,我在选项卡中为网格添加了上下文菜单,并使用 Sync
选项我试图滚动以查看在其他打开的选项卡中找到与关闭的 selected 日志的第一个匹配日志的位置。
问题:ExTabControl
无法在不同打开的选项卡中滚动到所需的日志项。
您需要按照 this 答案中的描述推导 TabControl
。使用该技术,将保留每个选项卡的可视化树。
请注意,如果您有很多选项卡,缓存将对性能产生重大影响。我建议最多将它用于 10 个标签。
If I do make use of default tab control i.e. TabControl
instead of ExTabControl
then it is working fine as expected. But if I move scroll in any of tab for dgvLogs
then it is reflecting in other tabs also.
TabControl
有两种用途,在this post上延伸:
- 当我们将
ItemsSource
绑定到项目列表,并且我们为每个项目设置了相同的 DataTemplate
时,TabControl
将只为所有项目创建一个“内容”视图. And when a different tab item is selected, the View
doesn't change but the backing DataContext
is bound to the viewmodel of the newly selected item.
Is it possible to update other tab content from code behind and let caching allow to re-cache changed UI elements?
更新不起作用的原因是另一个 WPF 优化,来自 UIElement.IsVisible:
Elements where IsVisible is false do not participate in input events (or commands), do not influence either the measure or arrange passes of layout, are not focusable, are not in a tab sequence, and will not be reported in hit testing.
您可以更改缓存元素的属性,但某些操作需要 UIElement
可见才能生效。
值得注意:
- 如果您在不可见的
DataGrid
上调用 ScrollIntoView
,它不会滚动到给定的对象。因此,链接项目中的 ScrollToSelectedBehavior
旨在滚动在此过程中可见的数据网格。
- 在
ExTabControl
的代码中,方法 UpdateSelectedItem
将非活动 contentpresenter 的可见性设置为折叠。
鉴于您已明确要求隐藏代码,
快速破解
TraceViewerView.xaml
<DataGrid IsVisibleChanged="dgvLogs_IsVisibleChanged" ... >
TraceViewerView.xaml.cs
private void dgvLogs_IsVisibleChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
{
if (sender is DataGrid dataGrid && dataGrid.IsVisible)
{
TraceViewerViewModel viewModel = (TraceViewerViewModel)DataContext;
if (viewModel.Log != null)
dataGrid.ScrollIntoView(viewModel.Log);
}
}
几点说明:
- 您现在可以删除行
local:ScrollToSelectedBehavior.SelectedValue="{Binding Log}"
,因为我们直接从视图模型中获取同步值。
- 这 是 一个 hack,视图被硬编码到您的视图模型中,有时可能会崩溃。
更好的方法
首先,为了保持我们的代码松散耦合,一个接口。
interface ISync
{
object SyncValue { get; }
}
TraceViewerModel.cs
public class TraceViewerViewModel : PropertyObservable, ITabItem, ISync
重命名Log
为SyncValue
,并替换原代码
private TraceLog synclog;
public TraceLog Log
{
get { return synclog; }
private set
{
synclog = value;
OnPropertyChanged();
}
}
和
public object SyncValue { get; set; }
基本上,我们用 Binding
换取一个界面。我在这个特定用例中选择界面的原因是,当你移动到它时你只需要检查一个选项卡的同步值(使一个完整的 Binding
有点矫枉过正)。
接下来,让我们创建一个 Behavior
来满足您的需求。
我将使用交互行为来代替附加的 属性,它提供了一种更加封装的方式来扩展功能 (requires System.Windows.Interactivity)。
ScrollToSyncValueBehavior.cs
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
namespace WpfApp1
{
public class ScrollToSyncValueBehavior : Behavior<DataGrid>
{
protected override void OnAttached()
{
this.AssociatedObject.IsVisibleChanged += OnVisibleChanged;
}
protected override void OnDetaching()
{
this.AssociatedObject.IsVisibleChanged -= OnVisibleChanged;
}
private static void OnVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (sender is DataGrid dataGrid && dataGrid.IsVisible)
{
ISync viewModel = dataGrid.DataContext as ISync;
if (viewModel?.SyncValue != null)
dataGrid.ScrollIntoView(viewModel.SyncValue);
}
}
}
}
TraceViewerView.xaml
<UserControl x:Class="WpfApp1.TraceViewerView"
...
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<DataGrid CanUserAddRows="false" GridLinesVisibility="None" AutoGenerateColumns="False"
ItemsSource="{Binding Logs}">
<i:Interaction.Behaviors>
<local:ScrollToSyncValueBehavior />
</i:Interaction.Behaviors>
...
</DataGrid>
</Grid>
</UserControl>
关于这个 SO 问题 here,是否可以从后面的代码更新其他选项卡内容并让缓存允许重新缓存已更改的 UI 元素?就像在场景中我已经更新了 DataGrid
某些事件 TabControl
上 tabs
的滚动索引,
dgvLogs.ScrollIntoView( log );
现在,由于选项卡已被缓存,并且当用户切换到 dgvLogs
所在的选项卡时,上面的更改不会反映出来。
编辑
我在主 window 中有选项卡控件 (ExTabControl
) 和多个包含 datagrid
的选项卡,其中显示了一些应用程序日志。像这样:
ExTabControl
像这样:
<controls:ExTabControl Grid.Row="1" ItemsSource="{Binding Tabs, Mode=OneWay}" >
<controls:ExTabControl.Resources>
<Style TargetType="{x:Type TabItem}">
</Style>
</controls:ExTabControl.Resources>
</controls:ExTabControl>
单个选项卡具有这样的数据网格:
<DataGrid Name="dgvLogs" ItemsSource="{Binding Logs}" VerticalScrollBarVisibility="Auto" FrozenColumnCount="4">
问题:
假设我在 ExTabControl
中有 3 个选项卡,selected 选项卡是 1,后面的代码使用 dgvLogs.ScrollIntoView( someInbetweenlog );
更新了选项卡 2 的滚动索引。理想情况下,如果我执行 select 选项卡 2,那么 dgvLogs
内的 select 滚动索引应该位于 someInbetweenlog
所在的位置。但不幸的是,标签 2 滚动不会根据更改后的代码移动..
如果我确实使用默认选项卡控件,即 TabControl
代替 ExTabControl
,那么它会按预期正常工作。但是如果我在 dgvLogs
的任何选项卡中移动滚动,那么它也会反映在其他选项卡中..
请添加评论,如果需要,我会 post 更多代码。
编辑 2
我创建了示例应用程序,试图在其中演示该问题。在这个应用程序中,我在选项卡中为网格添加了上下文菜单,并使用 Sync
选项我试图滚动以查看在其他打开的选项卡中找到与关闭的 selected 日志的第一个匹配日志的位置。
问题:ExTabControl
无法在不同打开的选项卡中滚动到所需的日志项。
您需要按照 this 答案中的描述推导 TabControl
。使用该技术,将保留每个选项卡的可视化树。
请注意,如果您有很多选项卡,缓存将对性能产生重大影响。我建议最多将它用于 10 个标签。
If I do make use of default tab control i.e.
TabControl
instead ofExTabControl
then it is working fine as expected. But if I move scroll in any of tab fordgvLogs
then it is reflecting in other tabs also.
TabControl
有两种用途,在this post上延伸:
- 当我们将
ItemsSource
绑定到项目列表,并且我们为每个项目设置了相同的DataTemplate
时,TabControl
将只为所有项目创建一个“内容”视图. And when a different tab item is selected, theView
doesn't change but the backingDataContext
is bound to the viewmodel of the newly selected item.
Is it possible to update other tab content from code behind and let caching allow to re-cache changed UI elements?
更新不起作用的原因是另一个 WPF 优化,来自 UIElement.IsVisible:
Elements where IsVisible is false do not participate in input events (or commands), do not influence either the measure or arrange passes of layout, are not focusable, are not in a tab sequence, and will not be reported in hit testing.
您可以更改缓存元素的属性,但某些操作需要 UIElement
可见才能生效。
值得注意:
- 如果您在不可见的
DataGrid
上调用ScrollIntoView
,它不会滚动到给定的对象。因此,链接项目中的ScrollToSelectedBehavior
旨在滚动在此过程中可见的数据网格。 - 在
ExTabControl
的代码中,方法UpdateSelectedItem
将非活动 contentpresenter 的可见性设置为折叠。
鉴于您已明确要求隐藏代码,
快速破解
TraceViewerView.xaml
<DataGrid IsVisibleChanged="dgvLogs_IsVisibleChanged" ... >
TraceViewerView.xaml.cs
private void dgvLogs_IsVisibleChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
{
if (sender is DataGrid dataGrid && dataGrid.IsVisible)
{
TraceViewerViewModel viewModel = (TraceViewerViewModel)DataContext;
if (viewModel.Log != null)
dataGrid.ScrollIntoView(viewModel.Log);
}
}
几点说明:
- 您现在可以删除行
local:ScrollToSelectedBehavior.SelectedValue="{Binding Log}"
,因为我们直接从视图模型中获取同步值。 - 这 是 一个 hack,视图被硬编码到您的视图模型中,有时可能会崩溃。
更好的方法
首先,为了保持我们的代码松散耦合,一个接口。
interface ISync
{
object SyncValue { get; }
}
TraceViewerModel.cs
public class TraceViewerViewModel : PropertyObservable, ITabItem, ISync
重命名Log
为SyncValue
,并替换原代码
private TraceLog synclog;
public TraceLog Log
{
get { return synclog; }
private set
{
synclog = value;
OnPropertyChanged();
}
}
和
public object SyncValue { get; set; }
基本上,我们用 Binding
换取一个界面。我在这个特定用例中选择界面的原因是,当你移动到它时你只需要检查一个选项卡的同步值(使一个完整的 Binding
有点矫枉过正)。
接下来,让我们创建一个 Behavior
来满足您的需求。
我将使用交互行为来代替附加的 属性,它提供了一种更加封装的方式来扩展功能 (requires System.Windows.Interactivity)。
ScrollToSyncValueBehavior.cs
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
namespace WpfApp1
{
public class ScrollToSyncValueBehavior : Behavior<DataGrid>
{
protected override void OnAttached()
{
this.AssociatedObject.IsVisibleChanged += OnVisibleChanged;
}
protected override void OnDetaching()
{
this.AssociatedObject.IsVisibleChanged -= OnVisibleChanged;
}
private static void OnVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (sender is DataGrid dataGrid && dataGrid.IsVisible)
{
ISync viewModel = dataGrid.DataContext as ISync;
if (viewModel?.SyncValue != null)
dataGrid.ScrollIntoView(viewModel.SyncValue);
}
}
}
}
TraceViewerView.xaml
<UserControl x:Class="WpfApp1.TraceViewerView"
...
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<DataGrid CanUserAddRows="false" GridLinesVisibility="None" AutoGenerateColumns="False"
ItemsSource="{Binding Logs}">
<i:Interaction.Behaviors>
<local:ScrollToSyncValueBehavior />
</i:Interaction.Behaviors>
...
</DataGrid>
</Grid>
</UserControl>