Trying to create a Datagrid and have users be able to remove a row by clicking an image. Example of the generated window:
However, I can't figure out how to connect clicking on the image with the generated cell next to it that is read in by a text file. Whenever I call for the value of that cell I can see List
, but not it contents.
DataGrid XAML Code:
<!-- Main Shared Drive Data Grid -->
<DataGrid HorizontalAlignment="Left"
Height="309"
VerticalAlignment="Top"
Width="550"
Margin="24,50,0,0"
Name="SDDataGrid"
Background="Black"
BorderBrush="#26534e"
BorderThickness="4"
Loaded="DataGrid_Loaded"
AutoGenerateColumns="True"
IsReadOnly="True"
RowHeaderWidth="0"
HeadersVisibility="Column"
ColumnWidth="*">
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="Background" Value="Black"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="#459289"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="BorderThickness" Value="0,0,2,2"/>
<Setter Property="BorderBrush" Value="#26534e"/>
<Setter Property="Padding" Value="4"/>
<Setter Property="Width" Value="Auto"/>
</Style>
<Style TargetType="{x:Type DataGridRow}">
<Setter Property="Background" Value="Black"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="#459289"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="BorderThickness" Value="1,1,0,2"/>
<Setter Property="BorderBrush" Value="#26534e"/>
<Setter Property="Padding" Value="4"/>
<Setter Property="Width" Value="Auto"/>
</Style>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="BorderThickness" Value="0,0,2,0"/>
<Setter Property="BorderBrush" Value="#26534e"/>
<Setter Property="Background" Value="Black"/>
<EventSetter Event="MouseDoubleClick" Handler="Do_Row_DoubleClick"/>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Width="58">
<DataGridTemplateColumn.HeaderTemplate>
<DataTemplate>
<TextBlock Text="Delete" Width="57"/>
</DataTemplate>
</DataGridTemplateColumn.HeaderTemplate>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Width="57">
<Button x:Name="deleteBtn" Width="53" Click="deleteBtn_Click">
<Button.Template>
<ControlTemplate>
<Image Source="Assets/trash.png"
Stretch="None"/>
</ControlTemplate>
</Button.Template>
</Button>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
C# Code:
// Shared Drive List
List<Drives> _list;
// Build The Drive List Object
public class Drives
{
public string Filepath { get; set; }
public Drives(string line)
{
string[] parts = line.Split(',');
this.Filepath = parts[0];
}
public string GetLine()
{
return this.Filepath.ToString();
}
}
// Loads DataGrid Of Window With Drive List
private void DataGrid_Loaded(object sender, RoutedEventArgs e)
{
var drives = new List<Drives>();
using (StreamReader reader = new StreamReader(@"..\..\Data\SDrives.txt"))
{
while (true)
{
string line = reader.ReadLine();
if (line == null)
{
break;
}
drives.Add(new Drives(line));
}
}
this._list = drives;
var grid = SDDataGrid;
grid.ItemsSource = drives;
}
// Adds User Submitted Filepath To Drive List And Reloads Window
private void Add_Btn_Click(object sender, RoutedEventArgs e)
{
var drives = new List<Drives>();
using (StreamWriter writer = new StreamWriter(@"..\..\Data\SDrives.txt", append: true))
{
writer.WriteLine(FilepathTextBox.Text);
}
using (StreamReader reader = new StreamReader(@"..\..\Data\SDrives.txt"))
{
while (true)
{
string line = reader.ReadLine();
if (line == null)
{
break;
}
drives.Add(new Drives(line));
}
}
this._list = drives;
var grid = SDDataGrid;
grid.ItemsSource = drives;
}
// Launches Filepath When User Double Clicks
private void Do_Row_DoubleClick(object sender, MouseButtonEventArgs e)
{
var cellInfo = SDDataGrid.CurrentCell;
{
var column = cellInfo.Column as DataGridBoundColumn;
if (column != null)
{
var element = new FrameworkElement() { DataContext = cellInfo.Item };
BindingOperations.SetBinding(element, TagProperty, column.Binding);
var cellValue = element.Tag;
if (Directory.Exists(@"" + cellValue))
{
Process.Start(@"" + cellValue);
}
else
{
System.Windows.MessageBox.Show(@"" + cellValue + " is not a valid filepath.");
}
}
}
}
private void deleteBtn_Click(object sender, RoutedEventArgs e)
{
var selected = SDDataGrid.SelectedItem;
Console.WriteLine(selected.ToString());
}
Text File:
C:\Users\Edward\Desktop\Projects
C:\Users\Edward\Desktop\School
Thanks!
I suggest to learn something about MVVM it gives you the power of WPF. And one of them is Command implementing ICommand
interface.
Let's assume that you don't want to dig into MVVM and want to get the solution here and now. I'll show it here and now and it's not MVVM but there's some approaches widely used in MVVM
If you want DataGrid
to update its source when you change the collection simply use ObservableCollection
instead of List
and fire PropertyChanged
event if you reassign the data property and keep the DataGrid
up to date automatically.
Let's go:
1) Implement INotifyPropertyChanged
interface for Window
.
public partial class MainWindow : Window, INotifyPropertyChanged
add the code to the Window
class
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
2) Set Window.DataContext
to itself to tell Binding
where it should find the target properties.
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
3) Make the data collection a Property, change List
to ObservableCollection
and fire PropertyChanged
event in set
clause. ObservableCollection
is almost the same as List
, so don't be afraid of using it.
private ObservableCollection<Drives> _drivesList; // backing field, never interact with it
// but use DrivesList instead
public ObservableCollection<Drives> DrivesList
{
get => _drivesList; // same as get { return _drivesList; } but shorter
set
{
_drivesList = value;
OnPropertyChanged();
}
}
4) And bind DataGrid
to it in xaml.
<DataGrid ItemsSource="{Binding DrivesList}"
...>
5) Here we're done with dynamic DataGrid
updates. And as result we have some code redundancy and let's make a cleanup.
private void DataGrid_Loaded(object sender, RoutedEventArgs e)
{
DrivesList = new ObservableCollection<Drives>();
using (StreamReader reader = new StreamReader(@"..\..\Data\SDrives.txt"))
{
while (!reader.EndOfStream)
{
string line = reader.ReadLine();
DrivesList.Add(new Drives(line));
}
}
}
private void Add_Btn_Click(object sender, RoutedEventArgs e)
{
string path = FilepathTextBox.Text;
using (StreamWriter writer = new StreamWriter(@"..\..\Data\SDrives.txt", append: true))
{
writer.WriteLine(path);
}
DrivesList.Add(new Drives(path));
}
The answer to the question:
6) But you'll get a problem with deletion because when you'll click a Delete button it will delete SelectedItem
but not the row where's button located. The only solution is pass to the deletion method the row where's the button located. And the most friendly solution is Command.
6.1) Here's ready helper class for easy commands use. Just put it into the project outside of Window
class: Right-click on the project in Solution Explerer, Select Add => Class => RelayCommand.cs. And add the following code there. Add it once and use as many times as you need.
Namespaces
using System;
using System.Windows.Input;
The class
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);
}
6.2) Implementation of command in Window
class
private ICommand _deleteCommand;
public ICommand DeleteCommand => _deleteCommand ?? (_deleteCommand = new RelayCommand(parameter =>
{
if (parameter is Drives drives)
{
DrivesList.Remove(drives);
using (StreamWriter writer = new StreamWriter(@"..\..\Data\SDrives.txt", append: false))
{
foreach(Drives drives in DrivesList)
{
writer.WriteLine(drives.Filepath);
}
}
}
}));
6.3) And usage in xaml
<Button Width="53"
Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}">
<Button.Template>
<ControlTemplate>
<Image Source="Assets/trash.png"
Stretch="None"/>
</ControlTemplate>
</Button.Template>
</Button>
Bonus part
7) Also you may replace Click
events with commands.
For example you have Add
button
<TextBox x:Name="FilepathTextBox"/>
<Button Content="Add" Click="Add_Btn_Click"/>
Replace with
<TextBox x:Name="FilepathTextBox"/>
<Button Content="Add"
Command="{Binding AddCommand}"
CommandParameter="{Binding Text,ElementName=FilepathTextBox}"/>
and implement it in Window
class
private ICommand _addCommand;
public ICommand AddCommand => _addCommand ?? (_addCommand = new RelayCommand(parameter =>
{
if (parameter is string path)
{
using (StreamWriter writer = new StreamWriter(@"..\..\Data\SDrives.txt", append: true))
{
writer.WriteLine(path);
}
DrivesList.Add(new Drives(path));
}
}));
In general Commands are not looks simpler than event handlers in Button
case but it have different features. Such as disable button with condition CanExecute
. You may try return false
in CanExecute
and the button become disabled. And as shown in DataGrid
you may also pass any object via CommandParameter
.
And the main feature is: Command can be located anywhere where you may set the Window's DataContext
but Event handler can be located only in Window
class. (Commands and Properties in MVVM are located in separate ViewModel class).
public ICommand AddCommand => _addCommand ?? (_addCommand = new RelayCommand(parameter =>
{
string path = (string)parameter;
using (StreamWriter writer = new StreamWriter(@"..\..\Data\SDrives.txt", append: true))
{
writer.WriteLine(path);
}
DrivesList.Add(new Drives(path));
},
parameter => parameter is string path && path.Length > 0));
// Here's CanExecute, any condition here may be used.
// This condition will prevent adding empty lines
// and will disable the Button if TextBox is empty, automatically