I get this error :
System.Windows.Data Error: 40 : BindingExpression path error: 'OpenPopupCommand' property not found on 'object' ''String' (HashCode=62725275)'. BindingExpression:Path=OpenPopupCommand; DataItem='String'
when I added a parameter to my command:
OpenPopupCommand = new RelayParamCommand((e) => PopupVisibility(FilterButton) );
VM:
private void PopupVisibility(object sender)
{
Console.WriteLine(sender.ToString());
PopupVisible ^= true;
}
Think is that I added Filter Button to Datagrid Headers which are generated automatically. Now I want to open popup when button is clicked. But think is that is not working, because i have to pass button by buttons x:Name to Popup PlacementTarget parameter.
<Page.DataContext>
<PDB:UsersViewModel x:Name="vm"/>
</Page.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!--Page Header info content-->
<Grid Grid.Row="0">
<StackPanel Orientation="Horizontal">
<TextBlock Margin="2" Text="{Binding ElementName=userPage, Path=Name}"/>
<TextBlock Margin="2" Text="{Binding SelectedUser.Name}"/>
<TextBlock Margin="2" Text="{Binding ElementName=myGrd, Path=CurrentColumn.DisplayIndex}"/>
<Button x:Name="mybtn"
Content="{Binding Filters.Count, Mode=OneWay}"
Visibility="{Binding Filters.Count, Converter={Wpf:VisibilityConverter}}"
/>
</StackPanel>
</Grid>
<!--Datagrid content-->
<DataGrid x:Name="myGrd"
SelectionMode="Single"
SelectionUnit="Cell"
CurrentItem="{Binding SelectedUser, Mode=TwoWay}"
CurrentColumn="{Binding CurrentColumn, Mode=TwoWay}"
IsReadOnly="True"
Grid.Row="1"
ItemsSource="{Binding FilteredUserList}"
AutoGenerateColumns="True"
CanUserAddRows="False"
>
<DataGrid.Resources>
<!--Popup-->
<ContextMenu x:Key="ContextMenu">
<ContextMenu.Items>
<MenuItem Header="Filter by Selection" Command="{Binding IncludeCommand, Source={x:Reference vm}}"/>
<MenuItem Header="Filter exclude Selection" Command="{Binding ExcludeCommand, Source={x:Reference vm}}"/>
<MenuItem Header="Remove all Filters" Command="{Binding RemoveAllFiltersCommand, Source={x:Reference vm}}" Visibility="{Binding Filters.Count, Source={x:Reference vm}, Converter={Wpf:VisibilityConverter}}"/>
</ContextMenu.Items>
</ContextMenu>
<!--Custom Datagrid header View-->
<Style TargetType="DataGridColumnHeader" x:Name="FilterHeader">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel>
<TextBox Margin="0,0,0,10" Width="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGridColumnHeader}}, Path=Width}" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding}" HorizontalAlignment="Center"/>
<Button Name="FilterButton"
Content="[-F-]"
Command="{Binding OpenPopupCommand}"
CommandParameter="{Binding ElementName=FilterButton}"/>
<Popup Name="MyPopup"
StaysOpen="False"
Placement="Right"
IsOpen="{Binding PopupVisible}"
PlacementTarget="{Binding FilterButton}">
<Border Background="White" BorderBrush="Black" Padding="5" BorderThickness="2" CornerRadius="5">
<StackPanel Orientation="Vertical">
</StackPanel>
</Border>
</Popup>
</StackPanel>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.Resources>
<DataGrid.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="ContextMenu" Value="{StaticResource ContextMenu}"/>
</Style>
</DataGrid.CellStyle>
</DataGrid>
</Grid>
By this approach i want to pass clicked button as parameter to VM and bind it to Popup PlacementTarget parameter. What i am doing wrong? Can i pass clicked button parameter? I know i break mvvm rules when passing view to vm, but how to achieve what i want, when i don't want to define every column in datagrid. Thank you
Your bindings have the DataContext
of the DataGridColumnHeader
as source which is the value of DataGridColumn.Header
. This value in your case is a string
and not your expected view model. This is the reason why your bindings doesn't resolve and you get the error message (which is exactly telling you this).
To fix the binding you have to find the next parent element that has the required DataContext
, which is I assume the DataGrid
:
<Button Name="FilterButton"
Content="[-F-]"
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=DataGrid}, Path=DataContext.OpenPopupCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Self}}"/>
When checking your code I can see the command which is bound to the Button
is toggling the PopupVisible
property. I therefore suggest to remove this view related code from the view model and replace the Button
with a ToggleButton
which binds directly to the Popup.IsOpen
:
<!--Custom Datagrid header View-->
<Style TargetType="DataGridColumnHeader">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel>
<TextBox Margin="0,0,0,10"
Width="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGridColumnHeader}}, Path=Width}" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding}"
HorizontalAlignment="Center" />
<ToggleButton Name="FilterButton"
Content="[-F-]" />
<Popup Name="MyPopup"
StaysOpen="False"
Placement="Right"
IsOpen="{Binding ElementName=FilterButton, Path=IsChecked}"
PlacementTarget="{Binding ElementName=FilterButton}">
<Border Background="White"
BorderBrush="Black"
Padding="5"
BorderThickness="2"
CornerRadius="5">
<StackPanel Orientation="Vertical">
</StackPanel>
</Border>
</Popup>
</StackPanel>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
To get the cell values of all rows contained in a single column where the column header value is the parameter and maps to a property name requires reflection. You need to bind a ListView
to the resulting column value collection which uses an ItemTemplate
to add a CheckBox
to the item. The final version should be as follows:
<!--Custom Datagrid header View-->
<Style TargetType="DataGridColumnHeader">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel>
<TextBox Margin="0,0,0,10"
Width="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGridColumnHeader}}, Path=Width}" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding RelativeSource={RelativeSource Self}}"
HorizontalAlignment="Center" />
<ToggleButton Name="FilterButton"
Content="[-F-]"
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=DataGrid}, Path=DataContext.(UsersViewModel.GenerateFilterViewItemsCommand)}"
CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=DataGridColumnHeader}, Path=Content}" />
<Popup Name="MyPopup"
StaysOpen="False"
Placement="Right"
IsOpen="{Binding ElementName=FilterButton, Path=IsChecked}"
PlacementTarget="{Binding ElementName=FilterButton}">
<Border Background="White"
BorderBrush="Black"
Padding="5"
BorderThickness="2"
CornerRadius="5">
<ListView
ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=DataGrid}, Path=DataContext.(UsersViewModel.FilterViewItems)}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsIncluded, Mode=OneWayToSource}"
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=DataGrid}, Path=DataContext.(UsersViewModel.IncludeItemCommand)}"
CommandParameter="{Binding}" />
<ContentPresenter Content="{Binding CellValue}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Border>
</Popup>
</StackPanel>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
To map the filter list to the actual items requires an additional type to hold the information:
Predicate.cs
public class FilterPredicate
{
public FilterPredicate(int rowIndex, object cellValue, object columnKey)
{
this.RowIndex = rowIndex;
this.CellValue = cellValue;
this.ColumnKey = columnKey;
}
public int RowIndex { get; set; }
public object CellValue { get; set; }
public bool IsIncluded { get; set; }
public object ColumnKey { get; set; }
}
You also need to add a collection for the filter view and one for the included item indices to the view model:
UsersViewModel.cs
public class UsersViewModel
{
public UsersViewModel()
{
this.FilterViewItems = new ObservableCollection<FilterPredicate>();
this.IncludedItemsIndex = new Dictionary<object, List<int>>();
}
// The binding source for the Popup filter view
public ObservableCollection<FilterPredicate> FilterViewItems { get; set; }
private Dictionary<object, List<int>> IncludedItemsIndex { get; set; }
public ICommand GenerateFilterViewItemsCommand => new RelayParamCommand((param) =>
{
var columnHeader = param as string;
this.IncludedItemsIndex.Remove(columnHeader);
this.FilterViewItems.Clear();
for (var rowIndex = 0; rowIndex < this.FilteredUserList.Count; rowIndex++)
{
var data = this.FilteredUserList.ElementAt(rowIndex);
var columnValue = data.GetType()
.GetProperty(columnHeader, BindingFlags.Public | BindingFlags.Instance)?
.GetValue(data);
if (columnValue != null)
{
this.FilterViewItems.Add(new FilterPredicate(rowIndex, columnValue, columnValue));
}
}
});
public ICommand IncludeItemCommand => new RelayParamCommand((param) =>
{
var predicate = param as FilterPredicate;
if (predicate.IsIncluded)
{
if (!this.IncludedItemsIndex.TryGetValue(predicate.ColumnKey, out List<int> includedIndices))
{
includedIndices = new List<int>();
this.IncludedItemsIndex.Add(predicate.ColumnKey, includedIndices);
}
includedIndices.Add(predicate.RowIndex);
}
else
{
if (this.IncludedItemsIndex.TryGetValue(predicate.ColumnKey, out List<int> includedIndices))
{
includedIndices.Remove(predicate.RowIndex);
}
}
// Apply the filter
CollectionViewSource.GetDefaultView(this.FilteredUserList).Filter =
data => this.IncludedItemsIndex.Values
.SelectMany(indices => indices)
.Contains(this.FilteredUserList.IndexOf(data as FilteredUser));
});
}