WPF中缓存View与ViewModel配对后绑定TabControl的实现问题
这个问题我之前做多窗口Tab切换的时候也踩过坑,核心是WPF里ContentPresenter的DataContext自动继承逻辑在搞鬼,先给你拆解下报错的原因,再给你两个可行的解决方案:
为什么会出现绑定错误?
当你写<ContentPresenter DataContext="{Binding ViewModel}" Content="{Binding View}" />的时候,WPF的绑定有个隐含逻辑:当你给ContentPresenter设置Content属性后,它的DataContext会自动替换成Content对象的DataContext(也就是你的View实例的DataContext),而不是保持原来的ViewModelPair。这时候再找ViewModel或View属性,自然就找不到了——因为当前DataContext已经变成了UserControl(你的View),不是ViewModelPair了,所以才会抛出“找不到属性”的绑定错误。
解决方案一:提前绑定View的DataContext(最直接推荐)
既然你已经缓存了View和ViewModel的配对,不如在创建ViewModelPair的时候就把View的DataContext设置好,这样后面绑定TabControl的时候就不用绕弯子了:
首先修改ViewModelPair的构造函数,提前关联DataContext:
public class ViewModelPair { public IViewModel ViewModel { get; set; } public UserControl View { get; set; } public ViewModelPair(IViewModel viewModel, UserControl view) { ViewModel = viewModel; View = view; // 提前把View的DataContext绑定到对应的ViewModel View.DataContext = viewModel; } }
然后修改TabControl的XAML,直接使用缓存的View实例:
<TabControl ItemsSource="{Binding ViewModels}"> <!-- 配置Tab页的标题,绑定ViewModel的属性(比如DisplayName) --> <TabControl.ItemTemplate> <DataTemplate DataType="{x:Type vm:ViewModelPair}"> <TextBlock Text="{Binding ViewModel.DisplayName}" /> </DataTemplate> </TabControl.ItemTemplate> <!-- 配置Tab页的内容,直接使用缓存的View --> <TabControl.ContentTemplate> <DataTemplate DataType="{x:Type vm:ViewModelPair}"> <!-- 直接绑定View属性,因为已经提前设置好DataContext了 --> <ContentControl Content="{Binding View}" /> </DataTemplate> </TabControl.ContentTemplate> </TabControl>
这种方式逻辑清晰,完全避开了ContentPresenter的绑定冲突,而且你的View实例是缓存好的,无论在TabControl还是其他窗口之间转移,都不会被重建,滚动位置、输入状态这些都会保留。
解决方案二:用RelativeSource强制获取原DataContext
如果不想在构造函数里提前设置DataContext,也可以通过RelativeSource让绑定跳过Content的DataContext继承,直接从父级获取ViewModelPair:
<TabControl ItemsSource="{Binding ViewModels}"> <TabControl.ContentTemplate> <DataTemplate DataType="{x:Type vm:ViewModelPair}"> <ContentPresenter <!-- 用RelativeSource找到当前ContentPresenter的原始DataContext(ViewModelPair) --> DataContext="{Binding DataContext.ViewModel, RelativeSource={RelativeSource Self}}" Content="{Binding DataContext.View, RelativeSource={RelativeSource Self}}" /> </DataTemplate> </TabControl.ContentTemplate> </TabControl>
这里的关键是用RelativeSource={RelativeSource Self}直接引用当前ContentPresenter的原始DataContext,而不是被Content覆盖后的DataContext,这样绑定就能正确找到ViewModel和View属性了。不过这种写法相对绕一点,不如方案一直观。
额外注意:避免TabControl销毁未激活的View
默认情况下,TabControl会把未选中的Tab内容卸载掉(销毁View)来节省内存,如果你要完全保留所有View的状态,需要加上两个设置:
<TabControl ItemsSource="{Binding ViewModels}" IsVirtualizing="False" VirtualizingStackPanel.IsVirtualizing="False"> <!-- 其他模板设置 --> </TabControl>
这样所有Tab的View实例都会一直保存在内存里,切换的时候不会被重建,状态也就不会丢失了。
内容来源于stack exchange

