Search code examples
c#wpfmvvmbindingtabitem

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


I'm working in MVVM and i have two TabItem created in XAML. In the first one, the popup is displayed, but in the second one when I press the button corresponding to a column, the popup is not displayed.

Here is my code with the working Popup:

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

and the tabItem that doesn't shows the popup:

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

...

The second tabItem is defined like the first one, but it seems that the Binding DataContext.ShowFilterCommand is not reached. I tried to enter with debug there but is not reached.

Here is the method:

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));
            }
        }
    }

What am i doing wrong?


Solution

  • If you swap the order of the two TabItems, or set SelectedIndex="1" on the TabControl, you'll find that the one that's initially visible is always the only one that works. I added PresentationTraceSources.TraceLevel=High to the Command binding, and found that the binding in the TabItem that's hidden on startup tries to resolve its source property initially, and doesn't try again when that TabItem becomes active.

    The trouble is that at the time of that first attempt at resolving the binding in the hidden tab item, none of the UI for that TabItem actually exists yet. Due to virtualization, it won't be created until it's needed. All that's created is its Header content, hanging in space. The DataGrid doesn't exist yet, so the AncestorType search never finds it. When the DataGrid is finally created, it seems there is no event that's raised to notify the Binding that it needs to repeat the ancestor search.

    The fix for this is easy: Create the header content via a DataTemplate. That's the correct way to do it anyhow. The DataTemplate will be instantiated when the DataGrid is shown.

    We'll use a value converter to create an identifier which tells the command what grid and column the user clicked. Notice that we're now giving each column a plain string for its Header property, and the TextBlock in the template changes to <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>
    

    Value converter:

    //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;
        }
    }
    

    Here's another solution if you have multiple columns bound to the same path, and you need to distinguish among them (in that case though, you could use the above solution for most of them, but slip in a customized template for the odd special case):

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

    Original Version

    It turns out that OP has multiple filtered columns per grid, so the solution below won't work: The command needs to know not just which grid, but which column in the grid. DataGridColumn isn't in the visual tree so we can't use that x:Name for the command parameter. We could use {RelativeSource AncestorType=DataGridColumnHeader} and write a converter that returns DataGridColumnHeader.Column.Binding.Path.Path -- but both grids have columns named ClassName. Next up is a multibinding that passes that ancestor, and also the grid itself. Since OP's original solution involved distinct content per column header anyway, I decided to do it the easy but verbose way, with multiple templates.

    I made one important change that will affect your code: Since the header content for both DataGrid columns is now being created by the same template, the name of the button is now the same for both. Therefore, CommandParameter is now bound to the name of the 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>
    

    If you want to vary the header content between the columns in the two different DataGrids, you can do that with bindings or create a second template if necessary.

    You could have also done this with a BindingProxy created in TabControl.Resources, but binding proxies are a last-ditch kludge that we use when there's no "correct" way to do something. In this case, the "correct" way is easy and direct and perfectly satisfactory.