Как удалять строку в datagrid, через кнопку, которая в нем самом?
Недавно увидел возможность вставить в datagrid кнопку в каждую строку. Выглядит вот так:
Может кто подсказать, как можно удалять с помощью этой кнопки именно ту строку, где нажал пользователь(например на строке 1 , пользователь нажал на эту кнопку и она спрашивает вы точно хотите удалить строку с кодом 1, и на ответ да она ее удаляет)? Ниже разметка именно кнопки.
<DataGridTemplateColumn IsReadOnly="True" Width="auto">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<!--
<Button Style="{StaticResource gridEditButton}">
<Icon:PackIconMaterial Kind="PencilOutline" Style="{StaticResource gridButtonIcon}"/>
</Button>-->
<Button Style="{StaticResource gridRemoveButton}" >
<Icon:PackIconMaterial Kind="DeleteOutline" Style="{StaticResource gridButtonIcon}"/>
</Button>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
Ответы (1 шт):
По последним двум говорите. Ну ок.
"Ручной" MVVM
Суть в том, что мы должны реализовать все самостоятельно, каждый метод, каждый класс.
Допустим у нас есть класс человека, который мы выводим, условно:
public class User { public User(string name, int age) => (Name, Age) = (name, age); public string Name { get; set; } public int Age { get; set; } }Далее у нас есть главный класс, который содержит в себе коллекцию людей и команду удаления:
public class MainViewModel { public MainViewModel() => DeleteUserCommand = new RelayCommand<User>(DeleteUser); public ObservableCollection<User> Users { get; } = new() { new User("Вася", 20), new("Петя", 18), new("Маша", 19) }; public ICommand DeleteUserCommand { get; } private void DeleteUser(User user) => Users.Remove(user); }Реализацию
RelayCommandдавать не буду, можете найти в интернете любую, их полно. Допустим в этом примере я взял код из этого ответа.Этот класс задан как
DataContextокна (как лучше), допустим для примера просто в конструкторе пишемDataContext = new MainViewModel();Теперь вид, делаем самый простой, чтоб была кнопка и вывод информации о человеке:
<ItemsControl ItemsSource="{Binding Users}"> <ItemsControl.ItemTemplate> <DataTemplate> <Grid Margin="0,5"> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <TextBlock> <Run Text="{Binding Name}" /> <Run Text="{Binding Age}" /> </TextBlock> <Button Grid.Column="1" Command="{Binding DeleteUserCommand}" CommandParameter="{Binding}" Content="Удалить" /> </Grid> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl>
В итоге у нас такой вид:
Но кнопки удалить не работают. Почему? Все дело в том, что при привязывании коллекции объектов к различным контролам по типу ItemsControl/ListBox и прочих, которые выводят нам коллекцию на экран, каждый элемент, с его собственным видом, будет иметь свой собственный DataContext на самого себя. В примере выше, команда DeleteUserCommand будет искаться в классе User, а не в главном классе. Чтобы это исправить, надо найти родителя текущего объекта и у него уже обратиться к DataContext и оттуда забрать наше свойство. Делается это примерно следующим образом:
Command="{Binding DataContext.DeleteUserCommand, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"
Как я и сказал выше, находим через RelativeSource с модом FindAncestor указанный тип родительского элемента (в примере ItemsControl), а у него уже забираем DataContext, который по умолчанию будет тем, что указан у окна (в примере это класс MainViewModel), ну а оттуда уже само свойство.
Все, при клике на кнопку "Удалить", будет вызываться метод DeleteUser из класса MainViewModel, который в свою очередь уже удалит объект из коллекции. Обратите внимание на тип коллекции, ведь для того, чтоб UI обновил свой вид, он должен знать, что в коллекции были сделаны изменения, за эти изменения отвечает INotifyCollectionChanged, который реализован по умолчанию в ObservableCollection<T> и BindingList<T>.
Касательно вывода сообщения об удалении, то тут тоже не все так просто и зависит от того, как вы пишете свой проект, да и с чем работаете. Допустим я вижу у вас элементы от Material Design, если он имеет свои окна, и он написан с поддержкой MVVM, то стоит читать его документацию.
"Автоматический" MVVM и использование сообщений
Тут уже более продвинутый подход, который с одной стороны покажется легким, а с другой может запутать, особенно если не знаете как все устроено. Суть в том, что в современном C# есть такая штука, которая зовется "Генерацией кода", где вы (или любой другой человек) можете написать некий генератор, который на основе некоторых критериев сам, за вас, создаст все то, что мы писали выше. Вот и Microsoft, спустя долгое время, решили сделать такой пакет, зовется он CommunityToolkit.Mvvm, достаточно установить, как простой NuGet пакет и использовать. Давайте теперь перепишем код:
Изменим
MainViewModel, удалив оттуда все лишнее:public partial class MainViewModel { public ObservableCollection<User> Users { get; } = new() { new User("Вася", 20), new("Петя", 18), new("Маша", 19) }; [RelayCommand] private void DeleteUser(User user) => Users.Remove(user); }Как видите, тут простой приватный метод, имеющий атрибут
[RelayCommand], это все, что требуется для создания команды, мы не должны иметь публичное свойство в классе, не должны писать ему реализацию, не должны хранить эту реализацию где либо, все это за нас теперь генерируется автоматически. А да, не забываем пометить класс какpartial, это позволяет разбивать один класс на несколько, что и требуется для генерации. Мы можем, кстати, посмотреть это все, найдя в зависимостях проекта анализатор:Вон наш класс, открываем и смотрим:
partial class MainViewModel { /// <summary>The backing field for <see cref="DeleteUserCommand"/>.</summary> [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.0.0.0")] private global::CommunityToolkit.Mvvm.Input.RelayCommand<global::WPF_net.User>? deleteUserCommand; /// <summary>Gets an <see cref="global::CommunityToolkit.Mvvm.Input.IRelayCommand{T}"/> instance wrapping <see cref="DeleteUser"/>.</summary> [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.0.0.0")] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public global::CommunityToolkit.Mvvm.Input.IRelayCommand<global::WPF_net.User> DeleteUserCommand => deleteUserCommand ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand<global::WPF_net.User>(new global::System.Action<global::WPF_net.User>(DeleteUser)); }Нам создало как приватное поле
private RelayCommand<User>? deleteUserCommand;, так и публичное свойство, которое реализует командуpublic IRelayCommand<User> DeleteUserCommand => deleteUserCommand ??= new RelayCommand<User>(new Action<User>(DeleteUser));.Запустив проект, мы получим аналогичный эффект, что и в первом примере, команды работают, все ок. Но помните я говорил про сообщения? Вот давайте их и реализуем:
Создадим класс, который будет описывать наше сообщение
public class DeleteUserMessage : ValueChangedMessage<User> { public DeleteUserMessage(User value) : base(value) {} }Тут главным является то, что мы наследуем его от
ValueChangedMessage, указав тип объекта, а также реализуем конструктор. То есть, это простой класс, который будет в себе содержать "сообщение" о том, что что-либо произошло, скажем так, это аналогEventArgsиз мира событий, только немного иначе реализованный.Дальше мы с вами переносим команду клика по кнопке в объект человека (ну или куда-либо еще, где нужна отправка сообщения), и отправляем сообщение мессенджеру, допустим так:
public partial class User { public User(string name, int age) => (Name, Age) = (name, age); public string Name { get; set; } public int Age { get; set; } [RelayCommand] private void DeleteUser() => WeakReferenceMessenger.Default.Send(new DeleteUserMessage(this)); }У нас простой метод, который помечен атрибутом
[RelayCommand], классpartial. Сам метод вызывает методSendи некого мессенджера (о нем почитаете в документации), передав туда наше сообщение и наш объект для удаления.В XAML возвращаем привязку обратно на
Command="{Binding DeleteUserCommand}", без указания параметров команды, и чего-либо еще.Ну а главный класс переделываем на следующий вид:
public partial class MainViewModel : IRecipient<DeleteUserMessage> { public MainViewModel() => WeakReferenceMessenger.Default.Register(this); public ObservableCollection<User> Users { get; } = new() { new User("Вася", 20), new("Петя", 18), new("Маша", 19) }; public void Receive(DeleteUserMessage message) => Users.Remove(message. Value); }Тут мы реализуем интерфейс
IRecipientс указанием типа нашего сообщения, этот интерфейс попросит создать метод, в котором мы получаем это сообщение. Также черезRegister()подписываемся на прослушивание сообщений, ну и все, готово. При клике по кнопке будет отправлено сообщение о том, что требуется удалить этот объект, а уже те, кто прослушивают наш канал сообщений, будут реагировать на это как положено.
В чем плюс этого подхода?
В том, что вы можете без труда отправлять сообщения любого вида, с любыми данными, из любого класса, как и можете подписываться на эти сообщения в любом месте, из любого класса и делать любую логику, какую только вам захочется. Вот надо вам в другом классе отравить данные в лог файл об удаление, просто подпишитесь там через Register, все, этого будет достаточно. По сути, это еще один уровень разделения вашего проекта на мало связанные друг с другом слои, чего и требует MVVM подход.
Вот вам и современный подход по созданию MVVM проекта, где мы в пару строк кода реализовали все то, что раньше требовало от нас десятка строк дополнительно. Но помните, такой подход стоит использовать тогда, когда уже полностью окрепли в стандартном написание всего этого, ведь без понимания того, как это работает, увы, будет трудно.
Удачи!

