c# - DataTemplate 在 WPF 中传递不正确的命令参数

标签 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 ( subview 模型的 View )

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

问题出在 SubView 中的 SelectionChanged EventTrigger

PrintCurrentItemsCommand 接受一个 ListView 作为参数,并通过执行以下方法打印其项目的计数:

private void PrintCurrentItems(ListView listView)
{
    System.Diagnostics.Debug.WriteLine("{0}: {1} items.", DisplayText, listView.Items.Count);
}

当我从一个 SubView(其中 ListView 中的某些项目被选中)导航到另一个 SubView 时,SelectionChanged 事件在第一个 SubViewListView 上触发。这会在正确的 SubViewModel 上执行 PrintCurrentItemsCommand,但会将新的(不正确的)ListView 作为参数传递。 (要么,要么事件由新的 ListView 触发,并且该命令使用旧 ListView 中的 DataContext。)

因此,虽然带有 DisplayText 的“Sub1”的 SubViewModel 在其 Models 集合中有 2 个项目,而“Sub2”有 3 个项目,我在“输出”窗口中看到以下内容:

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

显然,预期的行为是传递正确的 ListView

主要的混淆是,例如,“Sub1”的命令能够访问“Sub2”的 ListView

我读了一些关于 WPF caching templates 的内容,并认为我已经找到了在 DataTemplate 上设置 x:Shared = "False" 的解决方案,但这并没有改变任何东西。

对这种行为有解释吗?有解决办法吗?

最佳答案

我能够重现您所看到的行为:我在右侧 ListView 中选择一个项目,然后将选择更改为左侧 ListView 。调用命令时,在 Execute 方法中,! Object.ReferenceEquals(this, listView.DataContext)。我原以为他们是平等的。

对于 Command 的绑定(bind),它们仍然不相等:

<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 中的命令更为普遍。从 ListView 中获取项目集合很容易;在向某人传递项目集合之前需要有一个 ListView 真的很痛苦。始终接受尽可能不具体的参数,而不会搬起石头砸自己的脚。

从 MVVM 的角度来看,让 View 模型了解 UI 的任何具体知识被认为是非常糟糕的做法。如果 UI 设计团队后来决定应该使用 DataGrid 或 ListBox 而不是 ListView 怎么办?如果他们向您传递 Items,那完全不是问题。如果他们将 ListView 传递给您,他们必须向您发送一封电子邮件,要求您更改参数类型,然后就此与您进行协调,然后进行额外的测试等。所有这些都是为了适应参数实际上根本不需要是 ListView

关于c# - DataTemplate 在 WPF 中传递不正确的命令参数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47759033/

相关文章:

c# - 多个 SQL 查询 asp.net c#

c# - X509Store Certificates.Find FindByThumbprint

c# - 在 C# 项目中使用伪变量

c# - 在 DataGrid 中突出显示 SelectedItem 的行

c# - 在启动时显示默认 View (用户控件)

c# - 无法使用站点间连接从 Azure VM 连接 Azure SQL

WinForm应用程序中的WPF预编译资源?

c# - DataGrid RowDetails 中的按钮需要两次单击,解决方法会中断双击

c# - Storyboard中的模板绑定(bind) | ObjectAnimationUsingKeyFrames 不工作

.net - 如何在 ResourceDictionary 中存储 HTML 字符串?