Popup to edit complex property of a data grid - wpf

I am developing a WPF application that uses a datagrid to display a list of objects. A few of the columns are displaying properties with deeper information, I would like to display a summary value in the data grid, and then display a deeper window when clicked to allow editing properties.
I am part way to what I want, in that my data grid displays what I want and that I can make a dialog box appear when one of my special columns are clicked.
<DataGrid ItemsSource="{Binding Sequence.Steps}" AutoGenerateColumns="False" IsReadOnly="True" SelectionUnit="Cell">
<DataGrid.Resources>
<Style TargetType="DataGridCell">
<EventSetter Event="PreviewMouseDown" Handler="DataGrid_PreviewMouseDown" />
<Setter Property="Focusable" Value="False" />
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Header="Axis 1" Binding="{Binding Axes[0]}" ClipboardContentBinding="{Binding Axes[0].DestinationSP}" Width="*" />
<DataGridTextColumn Header="Axis 2" Binding="{Binding Axes[1]}" ClipboardContentBinding="{Binding Axes[1].DestinationSP}" Width="*"/>
<DataGridTextColumn Header="Axis 3" Binding="{Binding Axes[2]}" ClipboardContentBinding="{Binding Axes[2].DestinationSP}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
I have then defined a handler
private void DataGrid_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
var dlg = new Views.EditAxisMovement(CellValue); // Need to find a way to get Cell value so I can pass it
dlg.Owner = this;
dlg.ShowDialog();
}
However I cant find the right way to find CellValue to pass to the dialog box. Is there a nice and/or better way of handling this.

You could create a DataGridCellInfo and check the header of the column to map the cell against a property of your data item, e.g.:
private void DataGrid_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
DataGridCell cell = (DataGridCell)sender;
DataGridCellInfo cellInfo = new DataGridCellInfo(cell);
string header = cellInfo.Column.Header?.ToString();
int index = -1;
switch (header)
{
case "Axis 1":
index = 0;
break;
case "Axis 2":
index = 1;
break;
case "Axis 3":
index = 2;
break;
}
if (index != -1)
{
}
var dataItem = cellInfo.Item as YourClass;
object value = dataItem.Axes[index];
//...
}

DataGridCellInfo will just be the one for your circumstances.
var dlg = new Views.EditAxisMovement(new DataGridCellInfo((DataGridCell)sender).Item); // Need to find a way to get Cell value so I can pass it

Related

how to set the width of datatable column and how to set the color of datatable column upon some condition

IN xaml.cs file(WPF Application) I have created a DataTable with 3 columns
Wanted to set the second column's width in xaml.cs only.
Also, want to set the second columns first row background color to blue(only for the cell which is in first row and 2nd column).
Have created 3 columns as :
DataTable dt= new DataTable();
dt.Columns.Add(new DataColumn("ABC");
Similarly, have added 2 more columns.
Want to set the second columns width
I am not fairly certain, if this is what you are looking for, but it is what I would do
First: let's say you have created a basic DataTable and filled it with some values, like this:
DataTable dt = new DataTable();
dt.Columns.Add("Key", typeof(int));
dt.Columns.Add("Material", typeof(string));
dt.Columns.Add("Price per Kilo", typeof(int));
dt.Rows.Add(1, "CobbleStone", 34);
dt.Rows.Add(2, "Wooden Planks", 12);
dt.Rows.Add(3, "Iron Ingots", 56);
which looks like this in the debugger:
Second: Get some VisualElement to display your Data. I'd suggest using a DataGrid for. So go your your MainWindows.xaml and add a DataGrid to your Grid with 3 DataGridTextColumns like this:
<DataGrid>
</DataGrid>
Since we want to add custiom properties to our Columns, we have to add AutoGenerateColumns="False" to our DataGrid, if we don't the DataGrid will automatically generate its columns based on its ItemsSource. Since we won't get any autogenerated Columns now, we also have to add 3 Columns resembling the 3 columns from our DataTable:
<DataGrid AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Key" />
<DataGridTextColumn Header="Material" />
<DataGridTextColumn Header="Price per Kilo" />
</DataGrid.Columns>
</DataGrid>
Third: Next we have to set the ItemsSource of our DataGrid. Unfortunately a DataGrid can't process a DataTable, so we first have to convert our DataTable into something the DataGrid can read. Let's generate a new Class for this and call it MaterialModel, which looks like this:
using System.ComponentModel;
using System.Runtime.CompilerServices;
class Model : INotifyPropertyChanged
{
private int m_Key;
public int Key
{
get
{
return m_Key;
}
set
{
m_Key = value;
OnPropertyChanged("Key");
}
}
private string m_Name;
public string Name
{
get
{
return m_Name;
}
set
{
m_Name = value;
OnPropertyChanged("Name");
}
}
private int m_Price;
public int Price
{
get
{
return m_Price;
}
set
{
m_Price = value;
OnPropertyChanged("Price");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
It has Properties and a PropertyChangedEventHandler, which will notify your VisualElement when the Property changes.
Fourth: The DataGrid doesn't accept DataTables, but it accepts Lists and ObserableCollections. Use a List, if you don't want to ever add/change your items at runtime. I'll use an ObserableCollection, which neeeds using System.Collections.ObjectModel; to work.
Create a Property of your List and add a PropertyChangedEventHandler to MainWindow.
public partial class MainWindow : Window
{
private ObservableCollection<MaterialModel> m_MaterialList;
public ObservableCollection<MaterialModel> MaterialList
{
get
{
return m_MaterialList;
}
set
{
m_MaterialList = value;
OnPropertyChanged("MaterialList");
}
}
public MainWindow()
{
// [...]
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
The next step would be to convert your DataTable into a ObservableCollection, so iterate through your DataTable and Convert each Row to one of your Models, like this:
MaterialList = new ObservableCollection<MaterialModel>();
foreach(DataRow row in dt.Rows)
{
MaterialModel model = new MaterialModel
{
Key = int.Parse(row["Key"].ToString()),
Name = row["Material"].ToString(),
Price = int.Parse(row["Price per Kilo"].ToString()),
};
MaterialList.Add(model);
}
Fivth: Your List is filled with Models, the next step would be to tell your DataGrid how to use your List. First, bind your List to the ItemsSource auf your DataGrid, then bind each DataGridTextColumn to one of the Properties in your MaterialModel, like this:
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding MaterialList}">
<DataGrid.Columns>
<DataGridTextColumn Header="Key" Binding="{Binding Key}" />
<DataGridTextColumn Header="Material" Binding="{Binding Name}" />
<DataGridTextColumn Header="Price per Kilo" Binding="{Binding Price}" />
</DataGrid.Columns>
</DataGrid>
and you'll see the DataGrid works:
Sixth: The last step is to actually set the properties of your columns, which is pretty easy, your Requirements would look something like this:
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding MaterialList}">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Key" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Key}" Background="LightBlue"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Material" Binding="{Binding Name}" Width="300" />
<DataGridTextColumn Header="Price per Kilo" Binding="{Binding Price}" />
</DataGrid.Columns>
</DataGrid>
I haven't found a way to create a DataGrid completely in Code behind as you wanted, but this would be cosnidered bad practice anyway. WPF is designed to use this connection between xaml und c#.
If you want to manage your column properties in c# anyways, this would be a proper way to do it:
in your MainWindow.xaml.cs:
private double m_SecondColumnWidth;
public double SecondColumnWidth
{
get
{
return m_SecondColumnWidth;
}
set
{
m_SecondColumnWidth = value;
OnPropertyChanged("SecondColumnWidth");
}
}
public MainWindow()
{
SecondColumnWidth = 300;
}
XAML:
<!-- right beneath your Grid -->
<Grid.Resources>
<local:ViewModel x:Key="viewModel" />
</Grid.Resources>
<DataGridTextColumn Header="Material" Binding="{Binding Name}" Width="{Binding Source={StaticResource viewModel}, Path=SecondColumnWidth}" />
This isn't exactly what you wanted, but I hope it helps any way.

WPF DataGridComboBoxColumn binding

im having some trouble when trying to use a DataGridComboBoxColumn to update my entity framework
I have a datagrid that I am binding to a Custom Model (FunctionPrinterLookupModel), which is basically a lookup between Printers and Functions around the building. The Functions are all static, but I would like users to be able to select which printer they use for the function.
<DataGrid Grid.Row="1" x:Name="gridLookup" AutoGenerateColumns="False" Width="500" RowEditEnding="gridLookup_RowEditEnding" Margin="20">
<DataGrid.DataContext>
<Models:Printer/>
</DataGrid.DataContext>
<DataGrid.Columns>
<DataGridTextColumn Header="Function" Width="*" IsReadOnly="True" Binding="{Binding FunctionName}"/>
<!--<DataGridTextColumn Header="Printer" Width="*" Binding="{Binding PrinterName, UpdateSourceTrigger=PropertyChanged}"/>-->
<DataGridComboBoxColumn x:Name="ddlPrinters" Header="Printer" Width="*" SelectedValueBinding="{Binding PrinterID, Mode=TwoWay}" SelectedValuePath="{Binding PrinterID, Mode=TwoWay}" DisplayMemberPath="{Binding PrinterName}"/>
</DataGrid.Columns>
</DataGrid>
private void gridPrinters_RowEditEnding(object sender, DataGridRowEditEndingEventArgs e)
{
if (e.EditAction == DataGridEditAction.Commit)
{
Printer printer = (Printer)e.Row.Item;
if (printer.PrinterID != 0)
{
Printer printerDB = context.Printers.Where(s => s.PrinterID == printer.PrinterID).Single();
printerDB.PrinterName = printer.PrinterName;
context.SaveChanges();
}
else
{
Printer newPrinter = new Printer()
{
PrinterName = printer.PrinterName
};
context.Printers.Add(newPrinter);
context.SaveChanges();
}
}
RefreshPrintersGrid();
}
I am binding the DataGridComboBoxColumn in the code behind to an EF model containing list of Printers.
When the value has been selected and we trigger the RowEditEnding function, the value of the combobox is not updated in the FunctionPrinterLookupModel model. I feel like im tying myself in knots here and havent been able to find a solution that works from my hours of googling. Can any one help straighten me out?
You would be better off binding the combobox items source to a property in your ViewModel. Then bind the selected printer and in the ViewModel take action when the property changes.
<DataGridComboBoxColumn x:Name="ddlPrinters" Header="Printer" Width="*" ItemsSource="{Binding PrinterList}" SelectedItem="{Binding SelectedPrinter, Mode=TwoWay}" SelectedValuePath="PrinterID" DisplayMemberPath="PrinterName"/>
In ViewModel
Private PrinterInfo _SelectedPrinter { get; set; }
Publuc PrinterInfo SelectedPrinter
{
get
{
return _SelectedPrinter;
}
set
{
_SelectedPrinter = value;
//save value to db or other actions
}
}

Check if any of the selected cells is ReadOnly In a WPf datagrid

I have a grid that has both editable and read-only cells. On selection of cells, if at least one cell is editable (not read-only) I have to enable Cut and Paste icons from the toolbar.
I have bound the IsEnabled property of Cut/Paste icons to a property CanPerformCutPaste.
This is the code that is currently working.
private void dataGrid_SelectedCellsChanged(object sender, SelectedCellsChangedEventArgs e)
{
CanPerformCutPaste = dataGrid.SelectedCells.Any(c => !GetDataGridCell(c, dataGrid).IsReadOnly);
}
public static DataGridCell GetDataGridCell(DataGridCellInfo cellInfo, DataGrid grid)
{
if (cellInfo == null || grid == null)
return null;
grid.ScrollIntoView(cellInfo.Item as DataRowView, cellInfo.Column);
var cellContent = cellInfo.Column.GetCellContent(cellInfo.Item);
if (cellContent != null)
return (DataGridCell)cellContent.Parent;
return null;
}
Is there a better way to achieve this functionality? I don't want this to result in a performance issue for large number of cells.
Edit:
The columns are never set to read-only. Only some cells based on a condition are read-only. Therefore the column property cannot be checked.
You most likely want to check the last selected Cell, instead of checking all the selected cells each time, and besides checking the column property of the selected cell is enough, here a simple example:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Button Content="Copy/Past" Grid.Row="0" IsEnabled="{Binding CanPerformCutPaste}"></Button>
<DataGrid ItemsSource="{Binding Items}" x:Name="Dg" Grid.Row="1" AutoGenerateColumns="False" SelectionUnit="Cell" SelectedCellsChanged="Dg_OnSelectedCellsChanged">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" IsReadOnly="True"/>
<DataGridTextColumn Header="Location" Binding="{Binding Location}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
and the SelectedCellsChanged handler:
private void Dg_OnSelectedCellsChanged(object sender, SelectedCellsChangedEventArgs e)
{
if (Dg.SelectedCells.Any())
CanPerformCutPaste = Dg.SelectedCells.Last().Column.IsReadOnly;
}
Ps: I am sure you know that the vm should implements the INotifyPropertyChanged Interface
private bool _canPerformCutPaste ;
public bool CanPerformCutPaste
{
get
{
return _canPerformCutPaste;
}
set
{
if (_canPerformCutPaste == value)
{
return;
}
_canPerformCutPaste = value;
OnPropertyChanged();
}
}

Wpf selecteditem not working - Linq

i have one grid binded with some data, coming from database as bellow and one edit button
<DataGrid AutoGenerateColumns="False" Name="SParts_grid" HorizontalAlignment="Center" Margin="32,101,32,0" VerticalAlignment="Top" Height="187" Width="530" >
<DataGrid.Columns>
<DataGridTextColumn Header="Part No" Binding="{Binding Path=SPartID}" />
<DataGridTextColumn Header="Part Code" Width="85" Binding="{Binding Path=SPartCode}" />
<DataGridTextColumn Header="Part Name" Width="160" Binding="{Binding Path=SPartName}" />
<DataGridTextColumn Header="Model" Width="120" Binding="{Binding Path=ModelName}" />
<DataGridTextColumn Header="Location" Binding="{Binding Path=SPartLocation}" />
<DataGridCheckBoxColumn Header="Active" Width="58" Binding="{Binding Path=SPartActive}" />
</DataGrid.Columns>
</DataGrid>
<Button x:Name="btnEdit" Content="Edit" HorizontalAlignment="Left" Margin="105,323,0,0" VerticalAlignment="Top" Width="75" Click="btnEdit_Click"/>
all data fetched from db table called TblSpareParts just one column "ModelName" is from another table called TblBikeModels
so my code is below to fetch data
window loaded function
private void Window_Loaded(object sender, RoutedEventArgs e)
{
LoadParts();
}
private void LoadParts()
{
RST_DBDataContext conn = new RST_DBDataContext();
var AllPArts = (from s in conn.TblSpareParts
join m in conn.TblBikeModels on s.ModelID equals m.ModelID
select new { s.SPartName, s.SPartCode, s.SPartLocation, s.SPartID, m.ModelName }).ToArray();
SParts_grid.ItemsSource = AllPArts;
}
it works well but now if i need the selecteditem it does not work as below
private void btnEdit_Click(object sender, RoutedEventArgs e)
{
TblSparePart SelectedSPData = SParts_grid.SelectedItem as TblSparePart;
if (SelectedSPData == null)
{
MessageBox.Show("You Must Select a Part");
}
else
{
MessageBox.Show("Selected");
}
}
but when i use this LoadParts function then selecteditem works fine but it does not show the data in ModelName column
private void LoadParts()
{
RST_DBDataContext conn = new RST_DBDataContext();
List<TblSparePart> AllPArts = (from s in conn.TblSpareParts
select s).ToList();
SParts_grid.ItemsSource = AllPArts;
}
Basically problem is in LoadParts function
When you do select new { s.SPartName, s.SPartCode, s.SPartLocation, s.SPartID, m.ModelName }, you return an anonymous type, instead of TblSparePart object.
That's why the casting SParts_grid.SelectedItem as TblSparePart; returns null.
I think you have a UI design problem. Working with joined tables in DataGrid will definitely get duplicated data, that's what you may have to avoid. A better solution is to work with two DataGrids or (ComboBox and a DataGrid) one for the models table the other one is for items related to the model.
If you are working with Entity framework, then the task is much easier. You just have to point the binding to the related model table.
Take a look at this case :
public class Item
{
public int Id {get;set;}
public string Name {get;set;}
public Model Model {get;set;}
}
public class Model
{
public int Id {get;set;}
public string Name {get;set;}
}
Here you can do the as you did secondly. By loading only Items table in your DataGrid you can add a column that points to the Model Name property. It's going to be ready to be shown due to lazy loading that EF offers.
<DataGridTextColumn Header="Model" Width="120" Binding="{Binding Path=Model.name}" />

How to perform Single click checkbox selection in WPF DataGrid?

I have a DataGrid with first column as text column and second column as CheckBox column. What I want is, if I click the check box. It should get checked.
But, it takes two click to get selected, for first click the cell is getting selected, for the second clicks the check box is getting checked. How to make the check box to get checked/unchecked with a single click.
I'm using WPF 4.0. Columns in the DataGrid are AutoGenerated.
For single click DataGrid checkbox you can just put regular checkbox control inside DataGridTemplateColumn and set UpdateSourceTrigger=PropertyChanged.
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding Path=IsSelected, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
I solved this with the following Style:
<Style TargetType="DataGridCell">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="IsEditing" Value="True" />
</Trigger>
</Style.Triggers>
</Style>
It's of course possible to adapt this further for specific columns ...
First of, I know this is a pretty old question but I still thought I'd try and answer it.
I had the same problem a couple of days ago and came across a surprisingly short solution for it (see this blog). Basically, all you need to do is replace the DataGridCheckBoxColumn definition in your XAML with the following:
<DataGridTemplateColumn Header="MyCheckBoxColumnHeader">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox HorizontalAlignment="Center" VerticalAlignment="Center" IsChecked="{Binding Path=MyViewModelProperty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
The upside of this solution is obvious - it's XAML-only; thus it effectively refrains your from burdening your code-behind with additional UI logic.
To make Konstantin Salavatov's answer work with AutoGenerateColumns, add an event handler to the DataGrid's AutoGeneratingColumn with the following code:
if (e.Column is DataGridCheckBoxColumn && !e.Column.IsReadOnly)
{
var checkboxFactory = new FrameworkElementFactory(typeof(CheckBox));
checkboxFactory.SetValue(FrameworkElement.HorizontalAlignmentProperty, HorizontalAlignment.Center);
checkboxFactory.SetValue(FrameworkElement.VerticalAlignmentProperty, VerticalAlignment.Center);
checkboxFactory.SetBinding(ToggleButton.IsCheckedProperty, new Binding(e.PropertyName) { UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged });
e.Column = new DataGridTemplateColumn
{
Header = e.Column.Header,
CellTemplate = new DataTemplate { VisualTree = checkboxFactory },
SortMemberPath = e.Column.SortMemberPath
};
}
This will make all of DataGrid's auto-generated checkbox columns be "single click" editable.
Based on blog referenced in Goblin's answer, but modified to work in .NET 4.0 and with Row-Selection Mode.
Notice that it also speeds up DataGridComboBoxColumn editing - by entering edit mode and displaying dropdown on single click or text input.
XAML:
<Style TargetType="{x:Type DataGridCell}">
<EventSetter Event="PreviewMouseLeftButtonDown" Handler="DataGridCell_PreviewMouseLeftButtonDown" />
<EventSetter Event="PreviewTextInput" Handler="DataGridCell_PreviewTextInput" />
</Style>
Code-behind:
private void DataGridCell_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
DataGridCell cell = sender as DataGridCell;
GridColumnFastEdit(cell, e);
}
private void DataGridCell_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
DataGridCell cell = sender as DataGridCell;
GridColumnFastEdit(cell, e);
}
private static void GridColumnFastEdit(DataGridCell cell, RoutedEventArgs e)
{
if (cell == null || cell.IsEditing || cell.IsReadOnly)
return;
DataGrid dataGrid = FindVisualParent<DataGrid>(cell);
if (dataGrid == null)
return;
if (!cell.IsFocused)
{
cell.Focus();
}
if (cell.Content is CheckBox)
{
if (dataGrid.SelectionUnit != DataGridSelectionUnit.FullRow)
{
if (!cell.IsSelected)
cell.IsSelected = true;
}
else
{
DataGridRow row = FindVisualParent<DataGridRow>(cell);
if (row != null && !row.IsSelected)
{
row.IsSelected = true;
}
}
}
else
{
ComboBox cb = cell.Content as ComboBox;
if (cb != null)
{
//DataGrid dataGrid = FindVisualParent<DataGrid>(cell);
dataGrid.BeginEdit(e);
cell.Dispatcher.Invoke(
DispatcherPriority.Background,
new Action(delegate { }));
cb.IsDropDownOpen = true;
}
}
}
private static T FindVisualParent<T>(UIElement element) where T : UIElement
{
UIElement parent = element;
while (parent != null)
{
T correctlyTyped = parent as T;
if (correctlyTyped != null)
{
return correctlyTyped;
}
parent = VisualTreeHelper.GetParent(parent) as UIElement;
}
return null;
}
I've tried these suggestions, and plenty of others I've found on other sites, but none of them quite worked for me. In the end, I created the following solution.
I've created my own DataGrid-inherited control, and simply added this code to it:
public class DataGridWithNavigation : Microsoft.Windows.Controls.DataGrid
{
public DataGridWithNavigation()
{
EventManager.RegisterClassHandler(typeof(DataGridCell),
DataGridCell.PreviewMouseLeftButtonDownEvent,
new RoutedEventHandler(this.OnPreviewMouseLeftButtonDown));
}
private void OnPreviewMouseLeftButtonDown(object sender, RoutedEventArgs e)
{
DataGridCell cell = sender as DataGridCell;
if (cell != null && !cell.IsEditing && !cell.IsReadOnly)
{
DependencyObject obj = FindFirstControlInChildren(cell, "CheckBox");
if (obj != null)
{
System.Windows.Controls.CheckBox cb = (System.Windows.Controls.CheckBox)obj;
cb.Focus();
cb.IsChecked = !cb.IsChecked;
}
}
}
public DependencyObject FindFirstControlInChildren(DependencyObject obj, string controlType)
{
if (obj == null)
return null;
// Get a list of all occurrences of a particular type of control (eg "CheckBox")
IEnumerable<DependencyObject> ctrls = FindInVisualTreeDown(obj, controlType);
if (ctrls.Count() == 0)
return null;
return ctrls.First();
}
public IEnumerable<DependencyObject> FindInVisualTreeDown(DependencyObject obj, string type)
{
if (obj != null)
{
if (obj.GetType().ToString().EndsWith(type))
{
yield return obj;
}
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
foreach (var child in FindInVisualTreeDown(VisualTreeHelper.GetChild(obj, i), type))
{
if (child != null)
{
yield return child;
}
}
}
}
yield break;
}
}
What does all this do ?
Well, each time we click on any cell in our DataGrid, we see if the cell contains a CheckBox control within it. If it does, then we'll set the focus to that CheckBox and toggle it's value.
This seems to work for me, and is a nice, easily reusable solution.
It is disappointing that we need to write code to do this though. The explanation that the first mouse click (on a DataGrid's CheckBox) is "ignored" as WPF uses it to put the row into Edit mode might sound logical, but in the real-world, this goes against the way every real application works.
If a user sees a checkbox on their screen, they should be able to click on it once to tick/untick it. End of story.
Base on Jim Adorno answer and comments on his post, this is solution with MultiTrigger:
<Style TargetType="DataGridCell">
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsReadOnly" Value="False" />
<Condition Property="IsMouseOver" Value="True" />
</MultiTrigger.Conditions>
<Setter Property="IsEditing" Value="True" />
</MultiTrigger>
</Style.Triggers>
</Style>
There is a much simpler solution here.
<DataGridTemplateColumn MinWidth="20" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid>
<CheckBox VerticalAlignment="Center" HorizontalAlignment="Center"/>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
If you use DataGridCheckBoxColumn to implement, first click is to focus, second click is to check.
But using DataGridTemplateColumn to implement needs one click only.
The difference of using DataGridComboboxBoxColumn and implementation by DataGridTemplateColumn is also similar.
Yet another simple solution is to add this style to your DataGridColumn.The body of your style can be empty.
<DataGridCheckBoxColumn>
<DataGridCheckBoxColumn.ElementStyle>
<Style TargetType="CheckBox">
</Style>
</DataGridCheckBoxColumn.ElementStyle>
</DataGridCheckBoxColumn>
I solved with this:
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Viewbox Height="25">
<CheckBox IsChecked="{Binding TheProperty, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
</Viewbox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
The checkbox active on single click!
<Style x:Key="StilCelula" TargetType="DataGridCell">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="IsEditing"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Converter={StaticResource CheckBoxColumnToEditingConvertor}}" />
</Trigger>
</Style.Triggers>
<Style>
Imports System.Globalization
Public Class CheckBoxColumnToEditingConvertor
Implements IValueConverter
Public Function Convert(ByVal value As Object, ByVal targetType As Type, ByVal parameter As Object, ByVal culture As CultureInfo) As Object Implements IValueConverter.Convert
Try
Return TypeOf TryCast(value, DataGridCell).Column Is DataGridCheckBoxColumn
Catch ex As Exception
Return Visibility.Collapsed
End Try
End Function
Public Function ConvertBack(ByVal value As Object, ByVal targetType As Type, ByVal parameter As Object, ByVal culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
Throw New NotImplementedException()
End Function
End Class
Here an approach with an own column class that is based on the default DataGridCheckBoxColumn class and can be used like the normal one. Just copy/paste.
public class DataGridCheckBoxColumn : System.Windows.Controls.DataGridCheckBoxColumn
{
private static Style _noFocusEditElementStyle;
static DataGridCheckBoxColumn()
{
ElementStyleProperty.OverrideMetadata(typeof(DataGridCheckBoxColumn), new FrameworkPropertyMetadata(NoFocusEditElementStyle));
EditingElementStyleProperty.OverrideMetadata(typeof(DataGridCheckBoxColumn), new FrameworkPropertyMetadata(NoFocusEditElementStyle));
}
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property.Name == nameof(IsReadOnly))
{
ElementStyle = IsReadOnly ? DefaultElementStyle : NoFocusEditElementStyle;
EditingElementStyle = IsReadOnly ? DefaultElementStyle : NoFocusEditElementStyle;
}
}
public static Style NoFocusEditElementStyle
{
get
{
if (_noFocusEditElementStyle == null)
{
Style style = new Style(typeof(System.Windows.Controls.CheckBox));
// When not in edit mode, the end-user should not be able to toggle the state
style.Setters.Add(new Setter(UIElement.FocusableProperty, false));
style.Setters.Add(new Setter(System.Windows.Controls.CheckBox.HorizontalAlignmentProperty, HorizontalAlignment.Center));
style.Setters.Add(new Setter(System.Windows.Controls.CheckBox.VerticalAlignmentProperty, VerticalAlignment.Top));
style.Seal();
_noFocusEditElementStyle = style;
}
return _noFocusEditElementStyle;
}
}
}
Usage with Read/Write Property:
<myNamespace:DataGridCheckBoxColumn Header="Name"
Binding="{Binding Name, UpdateSourceTrigger=PropertyChanged}" />
Usage with ReadOnly Property:
<myNamespace:DataGridCheckBoxColumn Header="Name"
IsReadOnly="True"
Binding="{Binding Name, Mode=OneWay}" />
Explanation:
The default column class has two style properties one for the edit mode and one for the view.
In case of ReadOnly we use the view style in both cases
In the other case in the edit mode we set the edit style
Instead of EditElementStyle I prefered a style where the checkbox does not get the focus (NoFocusEditElementStyle) since this behavior looks a little weird

Resources