DataTemplate在WPF中传递错误的命令参数

时间:2017-12-11 18:24:06

标签: c# wpf xaml mvvm datatemplate

我在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"时找到了解决方案,但这没有改变任何内容。

这种行为有解释吗?它有办法吗?

2 个答案:

答案 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; }) 将始终返回新实例