Как сделать биндинг кнопки которая внутри элемента ListView WPF MVVM
У меня есть ListView, который выводит содержимое, всего 4 столбца: номер главы, заголовок, кнопка редактирования, кнопка удаления. При нажатии на кнопку мне нужно получить элемент, где она находится (например, номер главы или название главы). Я попытался сделать привязку через имя ListView и через FindAncestor, но ничего не получилось. Пожалуйста, помогите решить эту проблему или укажите на ошибки.
XAML:
<ListView Name="TableOfContents"
ItemsSource="{Binding Path=ContentList}"
Background="{x:Null}" Width="600"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.CanContentScroll="True"
BorderBrush="{x:Null}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<i:InvokeCommandAction
Command="{Binding Command}"
CommandParameter="{Binding ElementName=TableOfContents, Path=SelectedItem}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<ListView.View>
<GridView>
<GridViewColumn Width="50">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding ThemeID}" TextWrapping="Wrap"
Foreground="Black" FontSize="30"
TextAlignment="Center"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Width="460">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding ThemeName}" TextWrapping="Wrap"
Foreground="Black" FontSize="20"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Width="40">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Button Content="?" ToolTip="Редактировать"
Foreground="Black" FontSize="18"
Command="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type ItemsControl}},
Path=TableOfContentsPageViewModel.EditTheme}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Width="40">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Button Content="❌" ToolTip="Удалить"
Foreground="Black" FontSize="18"
Command="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type ItemsControl}},
Path=TableOfContentsPageViewModel.DeleteTheme}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
ViewModel:
class TableOfContentsPageViewModel:BaseViewModel, IPageViewModel
{
public string SearchedChapter { get; set; }
public Visibility CanEdit { get; set; } = Visibility.Hidden;
public Theme SelectedTheme { get; set; }
public ObservableCollection<Theme> ContentList { get; set; }
public TableOfContentsPageViewModel()
{
ContentList = new ObservableCollection<Theme>(TrainSQL_Commands.GetAllThemes());
CanEdit = CurrentUser.Role == "Administrator" ? Visibility.Visible : Visibility.Hidden;
}
private ICommand _editTheme;
public ICommand EditTheme
{
get
{
return _editTheme ?? (_editTheme = new RelayCommand(x =>
{
MessageBox.Show("Edit theory");
}));
}
}
private ICommand _deleteTheory;
public ICommand DeleteTheme
{
get
{
return _deleteTheory ?? (_deleteTheory = new RelayCommand(x =>
{
MessageBox.Show("Delete theory");
}));
}
}
}
Картинка: https://i.stack.imgur.com/wooBo.png
Ответы (2 шт):
Нашёл ошибку Если в XAML кнопки написать
Command="{Binding ElementName=TableOfContents, Path=DataContext.DeleteTheme}"
то всё работает
Написал пример. Так как вы знакомы с MVVM, объяснять, что делает каждый класс не буду, но добавлю несколько комментариев в код.
Я не использовал MvvmLight или другие пакеты, чтобы решить задачу, поэтому в код добавил 2 вспомогательных класса, которые были использованы. Один для INotifyPropertyChanged, второй для ICommand.
public class NotifyPropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter);
public void Execute(object parameter) => _execute(parameter);
}
public class MainViewModel : NotifyPropertyChanged
{
private TableOfContentsViewModel _contentList;
private bool _isAdmin;
public TableOfContentsViewModel ContentList // здесь хранится ToC
{
get => _contentList;
set
{
_contentList = value;
OnPropertyChanged();
}
}
public bool IsAdmin // права админа
{
get => _isAdmin;
set
{
_isAdmin = value;
OnPropertyChanged();
}
}
// пример асинхронной загрузки
private async Task LoadTableOfContentsAsync()
{
// ...
await sqlCommand.ExecuteNonQueryAsync();
// ...
}
public MainViewModel()
{
IsAdmin = true; // сделать админом
// загрузка ToC должна производиться из класса, отвечающего за работу с данными
// из конструктора это делать не рекомендуется
ContentList = new TableOfContentsViewModel()
{
new TableOfContentsItem() { ThemeID = 1, ThemeName = "Тема 1"},
new TableOfContentsItem() { ThemeID = 2, ThemeName = "Тема 2"},
new TableOfContentsItem() { ThemeID = 3, ThemeName = "Тема 3"},
new TableOfContentsItem() { ThemeID = 4, ThemeName = "Тема 4"},
new TableOfContentsItem() { ThemeID = 5, ThemeName = "Тема 5"},
new TableOfContentsItem() { ThemeID = 6, ThemeName = "Тема 6"},
new TableOfContentsItem() { ThemeID = 7, ThemeName = "Тема 7"}
};
// и вообще лучше это делать асинхронно, чтобы не морозить интерфейс, пока идет загрузка
//_ = LoadTableOfContentsAsync();
}
}
// вы не показали класс элемента списка, поэтому я придумал свой
public class TableOfContentsItem
{
public int ThemeID { get; set; }
public string ThemeName { get; set; }
public override string ToString() => ThemeID + " " + ThemeName;
}
// чтобы не плодить сущности, я просто унаследовался от коллекции для VM списка
public class TableOfContentsViewModel : ObservableCollection<TableOfContentsItem>
{
private ICommand _deleteItemCommand;
private ICommand _editItemCommand;
private ICommand _openItemCommand;
// эта команда реализована и работает, остальные команды приведены как демо
public ICommand DeleteItemCommand => _deleteItemCommand ?? (_deleteItemCommand = new RelayCommand(parameter =>
{
if (parameter is TableOfContentsItem item)
{
if (MessageBox.Show(item.ToString(), "Удалить?", MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No) == MessageBoxResult.Yes)
{
Remove(item);
}
}
}));
public ICommand EditItemCommand => _editItemCommand ?? (_editItemCommand = new RelayCommand(parameter =>
{
if (parameter is TableOfContentsItem item)
{
MessageBox.Show(item.ToString(), "Редактирование");
}
}));
public ICommand OpenItemCommand => _openItemCommand ?? (_openItemCommand = new RelayCommand(parameter =>
{
if (parameter is TableOfContentsItem item)
{
MessageBox.Show(item.ToString(), "Открытие");
}
}));
}
Этот ListView поддерживает динамическую ширину, то есть вы можете изменять размер окна и ширина списка будет меняться.
- добавлен триггер на видимость кнопок только при наведении мыши
- туда же воткнул проверку прав доступа через биндинг еще одним триггером на видимость
- добавлены
InputBindingsс примерами для мыши и клавиатуры - это теперь не таблица
- стиль кнопки вынесен отдельно для демонстрации использования общих стилей
<ListView ItemsSource="{Binding ContentList}"
Margin="100,10"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.CanContentScroll="True"
BorderThickness="0">
<ListView.InputBindings>
<KeyBinding Key="Return"
Command="{Binding ItemsSource.OpenItemCommand, RelativeSource={RelativeSource AncestorType=ListView}}"
CommandParameter="{Binding SelectedItem, RelativeSource={RelativeSource AncestorType=ListView}}"/>
</ListView.InputBindings>
<ListView.Resources>
<Style TargetType="Button" x:Key="TableOfContentsButtonStyle">
<Setter Property="Width" Value="40"/>
<Setter Property="Height" Value="40"/>
<Setter Property="FontSize" Value="18"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="Margin" Value="5,0"/>
<Setter Property="Visibility" Value="Hidden"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver, RelativeSource={RelativeSource AncestorType=ListBoxItem}}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListBoxItem}}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding DataContext.IsAdmin, RelativeSource={RelativeSource AncestorType=Window}}" Value="False">
<Setter Property="Visibility" Value="Hidden"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ListView.Resources>
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type local:TableOfContentsItem}">
<Grid>
<DockPanel>
<Grid Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"/>
<ColumnDefinition />
<ColumnDefinition Width="50"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<Grid.InputBindings>
<MouseBinding Gesture="LeftDoubleClick"
Command="{Binding ItemsSource.OpenItemCommand, RelativeSource={RelativeSource AncestorType=ListView}}"
CommandParameter="{Binding}"/>
</Grid.InputBindings>
<TextBlock Text="{Binding ThemeID}"
Width="50" Height="40"
TextWrapping="Wrap"
Foreground="Black" FontSize="30"
TextAlignment="Center"/>
<TextBlock Grid.Column="1" Text="{Binding ThemeName}"
TextWrapping="Wrap"
VerticalAlignment="Center"
Foreground="Black" FontSize="20"/>
<Button Grid.Column="2"
Style="{StaticResource TableOfContentsButtonStyle}"
Content="?"
ToolTip="Редактировать"
Command="{Binding ItemsSource.EditItemCommand, RelativeSource={RelativeSource AncestorType=ListView}}"
CommandParameter="{Binding}"/>
<Button Grid.Column="3"
Style="{StaticResource TableOfContentsButtonStyle}"
Content="❌"
ToolTip="Удалить"
Command="{Binding ItemsSource.DeleteItemCommand, RelativeSource={RelativeSource AncestorType=ListView}}"
CommandParameter="{Binding}"/>
</Grid>
</DockPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="MaxWidth" Value="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=ListView}}"/>
<Setter Property="Height" Value="60"/>
</Style>
</ListView.ItemContainerStyle>
</ListView>