我在WPF项目中有以下内容:
主窗口
<Window x:Class="DataTemplateEventTesting.Views.MainWindow"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
...
xmlns:vm="clr-namespace:DataTemplateEventTesting.ViewModels"
xmlns:vw="clr-namespace:DataTemplateEventTesting.Views">
<Window.DataContext>
<vm:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions> ... </Grid.ColumnDefinitions>
<ListView ItemsSource="{Binding SubViewModels}"
SelectedValue="{Binding MainContent, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type vm:SubViewModel}">
<TextBlock Text="{Binding DisplayText}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ContentControl Grid.Column="1" Content="{Binding MainContent}">
<ContentControl.Resources>
<DataTemplate x:Shared="False" DataType="{x:Type vm:SubViewModel}">
<vw:SubView />
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
</Grid>
</Window>
SubView(SubViewModel的视图)
<UserControl x:Class="DataTemplateEventTesting.Views.SubView"
...
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity">
<Grid>
<ListView ItemsSource="{Binding Models}">
<ListView.View> ... </ListView.View>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction CommandParameter="{Binding RelativeSource={RelativeSource AncestorType={x:Type ListView}}}"
Command="{Binding PrintCurrentItemsCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ListView>
</Grid>
</UserControl>
问题在于SelectionChanged
中的EventTrigger
SubView
。
PrintCurrentItemsCommand
接受ListView
作为参数,并通过执行以下方法打印其项目计数:
private void PrintCurrentItems(ListView listView)
{
System.Diagnostics.Debug.WriteLine("{0}: {1} items.", DisplayText, listView.Items.Count);
}
当我从一个SubView
(其ListView
中的某些项目被选中)导航到另一个SubView
时,SelectionChanged
事件会在ListView
上触发第一个SubView
。这会在正确的PrintCurrentItemsCommand
上执行SubViewModel
,但会将新的(不正确的)ListView
作为参数传递。 (或者,新事件ListView
正在触发该事件,该命令正在使用旧版DataContext
中的ListView
。)
因此,虽然SubViewModel
{1}}的“Sub1”在其DisplayText
集合中有2个项目,而“Sub2”有3个项目,但我在“输出”窗口中看到以下内容:
Models
显然,预期的行为是传递正确的Sub1: 2 items. // selected an item
Sub1: 3 items. // navigated to Sub2
Sub2: 3 items. // selected an item
Sub2: 2 items. // navigated to Sub1
Sub1: 2 items. // selected an item
Sub1: 3 items. // navigated to Sub2
Sub2: 3 items. // selected an item
Sub2: 2 items. // navigated to Sub1
。
主要的困惑是,例如,“Sub1”的命令可以访问“{2}”的ListView
。
我读了一些关于WPF caching templates的内容,并认为我在ListView
上设置x:Shared = "False"
时找到了解决方案,但这没有改变任何内容。
这种行为有解释吗?它有办法吗?
答案 0 :(得分:2)
我能够重现您正在看到的行为:我在右侧列表视图中选择一个项目,然后将选择更改为左侧列表视图。在调用命令时,在Execute方法中! Object.ReferenceEquals(this, listView.DataContext)
。我原以为他们是平等的。
对于Command
的绑定,它们仍然不相等:
<i:InvokeCommandAction
Command="{Binding DataContext.PrintCurrentItemsCommand, RelativeSource={RelativeSource AncestorType={x:Type ListView}}}"
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType={x:Type ListView}}}"
/>
我对这个实验并没有多大期待,但尝试的时间并不长。
不幸的是,我现在没有时间深入研究这个问题。我还没有能够找到System.Windows.Interactivity.InvokeCommandAction
的源代码,但它看起来好像在一连串的事件和更新伴随着变化,事情发生在错误的顺序。
以下代码几乎难以忍受,但它的行为与预期一致。你可以通过编写自己的行为来降低它的难度。它不需要像InvokeCommandAction
一样光荣地概括。如果不那么普遍,就不太可能以同样的方式行为不端,即使这样做了,你也可以获得源代码并且可以正确调试它。
SubView.xaml
<ListView
ItemsSource="{Binding Models}"
SelectionChanged="ListView_SelectionChanged"
>
<!-- snip -->
SubView.xaml.ds
private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var listView = sender as ListView;
var cmd = listView.DataContext?.GetType().GetProperty("PrintCurrentItemsCommand")?.
GetValue(listView.DataContext) as ICommand;
if (cmd?.CanExecute(listView) ?? false)
{
cmd.Execute(listView);
}
}
稍微偏离主题,这将是更可取的:
protected void PrintCurrentItems(System.Collections.IEnumerable items)
{
//...
XAML
<i:InvokeCommandAction
Command="{Binding PrintCurrentItemsCommand}"
CommandParameter="{Binding Items, RelativeSource={RelativeSource AncestorType={x:Type ListView}}}"
/>
背后的代码
if (cmd?.CanExecute(listView) ?? false)
{
cmd.Execute(listView.Items);
}
原因在于,将IEnumerable
作为参数的命令通常比期望将任何项集合打包在列表视图中的命令更有用。从列表视图中获取项目集合很容易;在你传递一个项目集合之前需要有一个listview是一个真正的痛苦。始终接受最不具体的参数,而不必在脚下拍摄。
从MVVM的角度来看,对于视图模型具有任何具体的UI知识,它被认为是非常糟糕的做法。如果UI设计团队稍后决定它应该使用DataGrid或ListBox而不是ListView,该怎么办?如果他们通过了您Items
,那么这完全没有问题。如果他们通过了您ListView
,他们必须向您发送一封电子邮件,要求您更改参数类型,然后与您进行协调,然后进行额外测试,等等。容纳一个实际上根本不需要ListView
的参数。
答案 1 :(得分:1)
事实证明,问题是由DataTemplate
的持久性造成的。
正如Ed Plunkett所观察到的那样,ListView
一直是DataContext
,只有DataContext
正在发生变化。我想,发生的事情是导航发生了,然后事件被触发了,而此时ListView
发生了变化 - 一个简单的属性变化。
在希望的行为中,旧DataTemplate
将触发事件,并执行第一个ViewModel命令,这将在导航后发生,因此,其项目将计为0.但是{ListView
1}}共享,第一个ListView
第二个RelativeSource
,因此其项目不计为0,它们已被第二个ViewModel中的项目替换。导航后会发生这种情况,因此预计ListView
将返回DataContext
,其中第二个ViewModel为DataTemplateSelector
。
我已设法通过使用自定义public class ViewSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (container is FrameworkElement element && item is SubViewModel)
{
return element.FindResource("subviewmodel_template") as DataTemplate;
}
return null;
}
}
类来覆盖此默认行为:
DataTemplate
ResourceDictionary
存储在<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DataTemplateEventTesting.Views"
xmlns:vm="clr-namespace:DataTemplateEventTesting.ViewModels">
<DataTemplate x:Shared="False" x:Key="subviewmodel_template" DataType="{x:Type vm:SubViewModel}">
<local:SubView />
</DataTemplate>
</ResourceDictionary>
中(在App.xaml中合并):
ResourceDictionary
事实证明,在x:Shared="False"
中,ResourceDictionary
具有我希望它具有的关键效果(显然这仅在<Window x:Class="DataTemplateEventTesting.Views.MainWindow"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
...
xmlns:vm="clr-namespace:DataTemplateEventTesting.ViewModels"
xmlns:vw="clr-namespace:DataTemplateEventTesting.Views">
<Window.DataContext>
<vm:MainWindowViewModel />
</Window.DataContext>
<Window.Resources>
<vw:ViewSelector x:Key="view_selector" />
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions> ... </Grid.ColumnDefinitions>
<ListView ItemsSource="{Binding SubViewModels}"
SelectedValue="{Binding MainContent, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type vm:SubViewModel}">
<TextBlock Text="{Binding DisplayText}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ContentControl Grid.Column="1" Content="{Binding MainContent}"
ContentTemplateSelector="{StaticResource view_selector}" />
</Grid>
</Window>
中有效 ) - 它根据ViewModel保持模板隔离。
主窗口现在写为:
DataTemplate
有趣的是,我发现在这个特定的例子中需要满足以下两个条件:
<强>一强>
ResourceDictionary
位于x:Shared="False"
DataTemplateSelector
。
<强>两个强>
使用<ContentControl ... ContentTemplate="{StaticResource subviewmodel_template}" />
。
例如,当我满足第一个条件并使用x:Shared="False"
时,问题占上风。
同样,当DataTemplateSelector
不存在时,Sub1: 2 items. // selected an item
Sub1: 0 items. // navigated to Sub2
Sub2: 3 items. // selected an item
Sub2: 0 items. // navigated to Sub1
Sub1: 2 items. // selected an item
Sub1: 0 items. // navigated to Sub2
Sub2: 3 items. // selected an item
Sub2: 0 items. // navigated to Sub1
不再有效。
一旦这两个条件到位,输出窗口就会显示:
DataTemplateSelector
这是我之前在不同类型的ViewModel之间切换时所预期的行为。
为什么选择DataTemplateSelector?
在阅读documentation for x:Shared之后,我至少有一个关于为什么x:Shared
似乎需要这个工作的理论。
如文件中所述:
在WPF中,资源的默认
true
条件为DataTemplateSelector
。此条件意味着任何给定的资源请求始终返回相同的实例。
这里的关键词是 request 。
不使用DataTemplateSelector
,WPF确定需要使用哪个资源。因此,它只需要获取一次 - 一个请求。
使用DataTemplateSelector
,无法确定,因为即使对于相同类型的ViewModel,DataTemplateSelector
内可能还有其他逻辑。因此,Content
强制x:Shared="False"
中的每次更改都会强制执行请求,ResourceDictionary
var allowTabFocus = false;
$(window).on('keydown', function(e) {
console.log(e);
$('*').removeClass('tab-focus');
if(e.keyCode === 9) {
allowTabFocus = true;
}
});
$('*').on('focus', function() {
if(allowTabFocus) {
$(this).addClass('tab-focus');
}
});
$(window).on('mousedown', function() {
$('*').removeClass('tab-focus');
allowTabFocus = false;
})
将始终返回新实例