C# - 弹出窗口不显示在 MVVM 的第二个选项卡项中

C# - Popup is not displayed in second tab item in MVVM

我在 MVVM 中工作,我在 XAML 中创建了两个 TabItem。在第一个中,显示弹出窗口,但在第二个中,当我按下对应于一列的按钮时,弹出窗口不显示。

这是我的工作弹出窗口代码:

<Viewbox>
    <Grid Height="359" Width="746">
        <Popup Name="popupFilter" Placement="MousePoint" IsOpen="{Binding IsFilterOpen, Mode=OneWay}" StaysOpen="True" Width="200">
            <Border Background="White" BorderBrush="Gray" BorderThickness="1,1,1,1">
                <StackPanel Margin="5,5,5,15">
                    <ListBox x:Name="listBoxPopupContent" 
                             Height="250" 
                             ItemsSource="{Binding FilterItems}" 
                             BorderThickness="0" 
                             ScrollViewer.VerticalScrollBarVisibility="Auto">
                        <ListBox.ItemTemplate>
                            <DataTemplate>
                                <CheckBox IsChecked="{Binding IsChecked}" 
                                          Content="{Binding Item}" 
                                          Command="{Binding DataContext.ApplyFiltersCommand, 
                                                RelativeSource={RelativeSource FindAncestor, 
                                                AncestorType={x:Type ListBox}}}"
                                          CommandParameter="{Binding IsChecked, 
                                                RelativeSource={RelativeSource Self}, 
                                                Mode=OneWay}"/>
                            </DataTemplate>
                        </ListBox.ItemTemplate>
                    </ListBox>
                </StackPanel>
            </Border>
        </Popup>

        <Grid HorizontalAlignment="Left" Height="261" Margin="0,63,0,0" VerticalAlignment="Top" Width="736">
            <TabControl HorizontalAlignment="Left" Height="175" VerticalAlignment="Top" Width="716" Margin="10,0,0,0">
                <TabItem Header="Class">
                    <DataGrid x:Name="ClassViewDataGrid" ItemsSource="{Binding FilteredClassViewItems}" 
                              AutoGenerateColumns="False"
                              IsReadOnly="True"
                              CanUserReorderColumns="True"
                              CanUserResizeColumns="True"
                              CanUserSortColumns="True">
                        <DataGrid.Columns>
                            <DataGridTextColumn Binding="{Binding ClassName}">
                                <DataGridTextColumn.Header>
                                    <StackPanel Orientation="Horizontal">
                                        <TextBlock Text="Class" />
                                        <Button Name="buttonClassViewClassFilter" Margin="10,0,0,0"                          
                                                Command="{Binding DataContext.ShowFilterCommand, 
                                                    RelativeSource={RelativeSource FindAncestor, 
                                                    AncestorType={x:Type DataGrid}}}"
                                                CommandParameter="{Binding Name, RelativeSource={RelativeSource Self}}">
                                            <Button.ContentTemplate>
                                                <DataTemplate>
                                                    <Image Source="/Images/filter.png" Width="10" Height="10" />
                                                </DataTemplate>
                                            </Button.ContentTemplate>
                                        </Button>
                                    </StackPanel>
                                </DataGridTextColumn.Header>

以及不显示弹出窗口的 tabItem:

<TabItem Header="Field">
    <DataGrid x:Name="FielsdViewDataGrid" ItemsSource="{Binding FilteredFieldViewItems}" 
              AutoGenerateColumns="False"
              IsReadOnly="True"
              CanUserReorderColumns="True"
              CanUserResizeColumns="True"
              CanUserSortColumns="True">
        <DataGrid.Columns>
            <DataGridTextColumn Binding="{Binding ClassName}">
                <DataGridTextColumn.Header>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="Class" />
                        <Button Name="buttonFieldViewClassFilter" Margin="10,0,0,0"                          
                                Command="{Binding DataContext.ShowFilterCommand, 
                                    RelativeSource={RelativeSource FindAncestor, 
                                    AncestorType={x:Type DataGrid}}}"
                                CommandParameter="{Binding Name, RelativeSource={RelativeSource Self}}">
                            <Button.ContentTemplate>
                                <DataTemplate>
                                    <Image Source="/Images/filter.png" Width="10" Height="10" />
                                </DataTemplate>
                            </Button.ContentTemplate>
                        </Button>
                    </StackPanel>
                </DataGridTextColumn.Header>

...

第二个tabItem的定义和第一个一样,但是好像没有达到Binding DataContext.ShowFilterCommand。我试图在那里进行调试,但没有达到。

方法如下:

private void ShowFilterCommandRaised(object obj)
{
    IsFilterOpen = !IsFilterOpen;
    str = obj;
    if (IsFilterOpen)
    {
        if (str.Equals("buttonClassViewClassFilter"))
        {
            FilterItems.Clear();
            foreach (var classView in classViewItems)
            {
                FilterItems.Add(new CheckedListItem<string>(classView.ClassName, true));
            }
        }

        if (str.Equals("buttonClassViewExtendsFilter"))
        {
            FilterItems.Clear();
            foreach (var classView in classViewItems)
            {
                FilterItems.Add(new CheckedListItem<string>(classView.Category, true));
            }
        }

        if (str.Equals("buttonFieldViewClassFilter"))
        {
            FilterItems.Clear();
            foreach (var fieldView in fieldViewItems)
            {
                FilterItems.Add(new CheckedListItem<string>(fieldView.ClassName, true));
            }
        }
    }

我做错了什么?

如果您调换两个 TabItem 的顺序,或在 TabControl 上设置 SelectedIndex="1",您会发现最初可见的始终是唯一有效的。我将 PresentationTraceSources.TraceLevel=High 添加到 Command 绑定中,发现启动时隐藏的 TabItem 中的绑定最初尝试解析其来源 属性,并且当该 TabItem 变为时不再尝试活跃。

问题在于,在第一次尝试解析隐藏选项卡项中的绑定时,该 TabItem 的 UI 的 none 实际上还存在。由于虚拟化,它不会在需要时创建。所创建的只是它的 Header 内容,挂在 space 中。 DataGrid 尚不存在,因此 AncestorType 搜索永远找不到它。最终创建 DataGrid 时,似乎没有引发任何事件来通知 Binding 它需要重复祖先搜索。

解决这个问题很简单:通过 DataTemplate 创建 header 内容。无论如何,这是正确的方法。 DataGrid 显示时将实例化 DataTemplate。

我们将使用一个值转换器来创建一个标识符,该标识符告诉命令用户单击了哪个网格和列。请注意,我们现在为每一列的 Header 属性 提供一个纯字符串,并且模板中的 TextBlock 更改为 <TextBlock Text="{Binding}" />.

XAML

<TabControl HorizontalAlignment="Left" Height="175" VerticalAlignment="Top" Width="716" Margin="10,0,0,0">
    <TabControl.Resources>
        <local:GetColumnIdentifier x:Key="GetColumnIdentifier" />
        
        <DataTemplate x:Key="FilterColumnHeaderTemplate">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding}" />
                <Button 
                    Margin="10,0,0,0"
                    Command="{Binding DataContext.ShowFilterCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}}" 
                    CommandParameter="{Binding Converter={StaticResource GetColumnIdentifier}, RelativeSource={RelativeSource Self}}"
                    >
                    <Button.ContentTemplate>
                        <DataTemplate>
                            <Image Source="/Images/filter.png" Width="10" Height="10" />
                        </DataTemplate>
                    </Button.ContentTemplate>
                </Button>
            </StackPanel>
        </DataTemplate>
    </TabControl.Resources>
    <TabItem Header="Class">
        <DataGrid 
            x:Name="ClassViewDataGrid" 
            ItemsSource="{Binding FilteredClassViewItems}" 
            AutoGenerateColumns="False"
            IsReadOnly="True"
            CanUserReorderColumns="True"
            CanUserResizeColumns="True"
            CanUserSortColumns="True"
            >
            <DataGrid.Columns>
                <DataGridTextColumn 
                    Header="Class"
                    Binding="{Binding ClassName}" 
                    HeaderTemplate="{StaticResource FilterColumnHeaderTemplate}"
                    />
            </DataGrid.Columns>
        </DataGrid>
    </TabItem>
    <TabItem Header="Field">
        <DataGrid 
            x:Name="FielsdViewDataGrid" 
            ItemsSource="{Binding FilteredFieldViewItems}" 
            AutoGenerateColumns="False"
            IsReadOnly="True"
            CanUserReorderColumns="True"
            CanUserResizeColumns="True"
            CanUserSortColumns="True"
            >
            <DataGrid.Columns>
                <DataGridTextColumn 
                    Header="Class"
                    Binding="{Binding ClassName}" 
                    HeaderTemplate="{StaticResource FilterColumnHeaderTemplate}"
                    />
            </DataGrid.Columns>
        </DataGrid>
    </TabItem>
</TabControl>

值转换器:

//using System.Windows.Markup.Primitives;
//using System.Windows.Controls.Primitives;

public class GetColumnIdentifier : IValueConverter
{
    private static T GetVisualAncestor<T>(DependencyObject obj)
        where T : DependencyObject
    {
        while (obj != null)
        {
            if (obj is T)
                return obj as T;
            else
                obj = VisualTreeHelper.GetParent(obj);
        }

        return null;
    }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        //  Not all DataGridColumn subclasses have a Binding property. 
        var header = GetVisualAncestor<DataGridColumnHeader>((DependencyObject)value);
        var datagrid = GetVisualAncestor<DataGrid>(header);
        
        if (header?.Column != null)
        {
            MarkupObject markupObject = MarkupWriter.GetMarkupObjectFor(header.Column);
            var bindingProp = markupObject.Properties.FirstOrDefault(p => p.Name == "Binding");

            if (bindingProp?.Value is Binding binding)
            {
                return $"{datagrid?.Name}.{binding.Path.Path}";
            }
        }

        return null;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return null;
    }
}

如果您有多个列绑定到同一个路径,并且您需要区分它们(在这种情况下,您可以对大多数列使用上述解决方案,但为奇怪的特殊情况):

<TabControl HorizontalAlignment="Left" Height="175" VerticalAlignment="Top" Width="716" Margin="10,0,0,0">
    <TabItem Header="Class">
        <DataGrid 
            x:Name="ClassViewDataGrid" 
            ItemsSource="{Binding FilteredClassViewItems}" 
            AutoGenerateColumns="False"
            IsReadOnly="True"
            CanUserReorderColumns="True"
            CanUserResizeColumns="True"
            CanUserSortColumns="True"
            >
            <DataGrid.Columns>
                <DataGridTextColumn 
                    Header="Class"
                    Binding="{Binding ClassName}" 
                    >
                    <DataGridTextColumn.HeaderTemplate>
                        <DataTemplate>
                            <StackPanel Orientation="Horizontal">
                                <TextBlock Text="{Binding}" />
                                <Button 
                                    Name="buttonClassViewClassFilter" 
                                    Margin="10,0,0,0"
                                    Command="{Binding DataContext.ShowFilterCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}}" 
                                    CommandParameter="{Binding Name, RelativeSource={RelativeSource Self}}"
                                    >
                                    <Button.ContentTemplate>
                                        <DataTemplate>
                                            <Image Source="/Images/filter.png" Width="10" Height="10" />
                                        </DataTemplate>
                                    </Button.ContentTemplate>
                                </Button>
                            </StackPanel>
                        </DataTemplate>
                    </DataGridTextColumn.HeaderTemplate>
                </DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
    </TabItem>
    <TabItem Header="Field">
        <DataGrid 
            x:Name="FielsdViewDataGrid" 
            ItemsSource="{Binding FilteredFieldViewItems}" 
            AutoGenerateColumns="False"
            IsReadOnly="True"
            CanUserReorderColumns="True"
            CanUserResizeColumns="True"
            CanUserSortColumns="True"
            >
            <DataGrid.Columns>
                <DataGridTextColumn 
                    Header="Class"
                    Binding="{Binding ClassName}" 
                    >
                    <DataGridTextColumn.HeaderTemplate>
                        <DataTemplate>
                            <StackPanel Orientation="Horizontal">
                                <TextBlock Text="{Binding}" />
                                <Button 
                                    Name="buttonFieldViewClassFilter" 
                                    Margin="10,0,0,0"
                                    Command="{Binding DataContext.ShowFilterCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}}" 
                                    CommandParameter="{Binding Name, RelativeSource={RelativeSource Self}}"
                                    >
                                    <Button.ContentTemplate>
                                        <DataTemplate>
                                            <Image Source="/Images/filter.png" Width="10" Height="10" />
                                        </DataTemplate>
                                    </Button.ContentTemplate>
                                </Button>
                            </StackPanel>
                        </DataTemplate>
                    </DataGridTextColumn.HeaderTemplate>
                </DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
    </TabItem>
</TabControl>

原版

事实证明,OP 每个网格有多个过滤列,因此下面的解决方案不起作用:该命令不仅需要知道哪个网格,还需要知道网格中的哪一列。 DataGridColumn 不在可视化树中,因此我们不能将 x:Name 用作命令参数。我们可以使用 {RelativeSource AncestorType=DataGridColumnHeader} 并编写一个 returns DataGridColumnHeader.Column.Binding.Path.Path 的转换器—​​—但是两个网格都有名为 ClassName 的列。接下来是传递该祖先以及网格本身的多重绑定。由于 OP 的原始解决方案涉及每列不同的内容 header 无论如何,我决定使用多个模板以简单但冗长的方式进行。

我做了一个会影响您的代码的重要更改: 由于两个 DataGrid 列的 header 内容现在由同一模板创建,名称两者的按钮现在相同。因此,CommandParameter 现在绑定到 DataGrid.

的名称
<TabControl HorizontalAlignment="Left" Height="175" VerticalAlignment="Top" Width="716" Margin="10,0,0,0">
    <TabControl.Resources>
        <DataTemplate x:Key="FilterColumnHeaderTemplate">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding}" />
                <Button 
                    Name="buttonClassFilter" 
                    Margin="10,0,0,0"
                    Command="{Binding DataContext.ShowFilterCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}}" 
                    CommandParameter="{Binding Name, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}}"
                    >
                    <Button.ContentTemplate>
                        <DataTemplate>
                            <Rectangle Fill="DeepSkyBlue" Width="10" Height="10" />
                        </DataTemplate>
                    </Button.ContentTemplate>
                </Button>
            </StackPanel>
        </DataTemplate>
    </TabControl.Resources>
    <TabItem Header="Class">
        <DataGrid 
            x:Name="ClassViewDataGrid" 
            ItemsSource="{Binding FilteredClassViewItems}" 
            AutoGenerateColumns="False"
            IsReadOnly="True"
            CanUserReorderColumns="True"
            CanUserResizeColumns="True"
            CanUserSortColumns="True"
            >
            <DataGrid.Columns>
                <DataGridTextColumn 
                    Header="Class"
                    Binding="{Binding ClassName}" 
                    HeaderTemplate="{StaticResource FilterColumnHeaderTemplate}" 
                    />
            </DataGrid.Columns>
        </DataGrid>
    </TabItem>
    <TabItem Header="Field">
        <DataGrid 
            x:Name="FielsdViewDataGrid" 
            ItemsSource="{Binding FilteredFieldViewItems}" 
            AutoGenerateColumns="False"
            IsReadOnly="True"
            CanUserReorderColumns="True"
            CanUserResizeColumns="True"
            CanUserSortColumns="True"
            >
            <DataGrid.Columns>
                <DataGridTextColumn
                    Header="Class"
                    Binding="{Binding ClassName}" 
                    HeaderTemplate="{StaticResource FilterColumnHeaderTemplate}" 
                    />
            </DataGrid.Columns>
        </DataGrid>
    </TabItem>
</TabControl>

如果您想改变两个不同 DataGrid 中列之间的 header 内容,您可以使用绑定来实现,或者在必要时创建第二个模板。

您也可以在 TabControl.Resources 中创建 done this with a BindingProxy,但绑定代理是 last-ditch 我们在没有“正确”的方式做某事时使用的拼凑物。在这种情况下,“正确”的方法简单直接,令人满意。