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