2
votes

I want to cancel changes in a row when user click ✖ button.

enter image description here source code

private void CancelChangesButton_Click(object sender, RoutedEventArgs e)
{
    datagrid.CancelEdit();
}

CancelEdit() works great, until... my DateConverter can't ConvertBack a string. The same behaviour occurs when property setter of a ViewModel throws an exception. I can't do anything in DataGrid. The only way is to press the ESC key when cursor is in the red cell.

I try other things:

datagrid.CancelEdit(DataGridEditingUnit.Row);
datagrid.CancelEdit(DataGridEditingUnit.Cell);
datagrid.CommitEdit();
datagrid.IsReadOnly = true;
// Add new item

Nothing happened.

So I started to dig in the .NET Framework sources and I found this:

public class DataGrid : MultiSelector
...
    public bool CancelEdit(DataGridEditingUnit editingUnit)
    {
        return EndEdit(CancelEditCommand, CurrentCellContainer, editingUnit, true);
    }

-> .NET Reference source

The most important thing here is CurrentCellContainer that gets the value from CurrentCell. Next, I discovered that CurrentCell is following the focus. When I click ✖ button, CurrentCell changes to cell in Action column, and when I click outside DataGrid, CurrentCell changes to null.

So, I have to change CurrentCell to cell with validation error, and than invoke CancelEdit(). Do I think right?

How to find all cells with a validation error?

Is there another way to cancel editing?

2

2 Answers

1
votes

The red cell visualizes a validation error. You can't cancel the edit mode as long there are validation errors (except the user presses the Escape key).

The only solution is to manually resolve the errors by simply reverting the input.

The algorithm is as followed:

  1. Get the current cell
  2. Get the container of the current cell
  3. Check if container (cell) has validation errors. If yes continue with step 4, else just cancel the edit (jump to step 9)
  4. Get the edit TextBox of the cell template
  5. Identify the binding source's (the data item) property of the edit TextBox.Text property
  6. Get the value of the property (using reflection for generic behavior)
  7. Revert the content
  8. Move keyboard focus back to the edit TextBox which will trigger the re-validation and defines the target cell for the cancellation.
  9. Cancel the edit

Implementation:

private void CancelChangesButton_Click(object sender, RoutedEventArgs e)
{
  DependencyObject cellItemContainer = this.datagrid.ItemContainerGenerator.ContainerFromItem(
    (this.datagrid.CurrentCell.Item as SomethingItem));

  // If the current cell has validation errors find the edit TextBox child control
  if (Validation.GetHasError(cellItemContainer) && TryFindChildElement(cellItemContainer, out TextBox editTextBox))
  {
    // Get the property name of he binding source
    var propertyName = (editTextBox.BindingGroup.BindingExpressions.FirstOrDefault() as BindingExpression)?.ResolvedSourcePropertyName ?? string.Empty;

    // Use reflection to get the value of the binding source
    object value = this.datagrid.CurrentCell.Item.GetType().GetProperty(propertyName).GetValue(this.datagrid.CurrentCell.Item);

    // Check which ToString() to invoke
    editTextBox.Text = value is DateTime date 
      ? date.ToShortDateString() 
      : value.ToString();

    // Trigger validation and define which cell to cancel the edit
    // This is required because the edit TexBox lost focus
    Keyboard.Focus(editTextBox);
  }

  this.datagrid.CancelEdit();
}

// Traverses the visual tree to find a child element of type TElement
private bool TryFindVisualChild<TChild>(DependencyObject parent, out TChild resultElement) where TChild : DependencyObject
{
  resultElement = null;
  for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
  {
    DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);

    if (childElement is Popup popup)
    {
      childElement = popup.Child;
    }

    if (childElement is TChild)
    {
      resultElement = childElement as TChild;
      return true;
    }

    if (TryFindVisualChild(childElement, out resultElement))
    {
      return true;
    }
  }

  return false;
}
1
votes

I realize that I don't need to find a cell with a validation error. All I need to do is call CancelEdit() on all cells.

private void CancelChangesButton_Click(object sender, RoutedEventArgs e)
{
    var cc = dataGrid.CurrentCell;
    foreach (var col in datagrid.Columns)
    {
        datagrid.CurrentCell = new DataGridCellInfo(datagrid.CurrentItem, col);
        datagrid.CancelEdit();
    }
    dataGrid.CurrentCell = cc;
}

It's also working with DataGridTemplateColumn. solution code


However, if you wants to find which cells contain a validation error, you need to look deeper. Thanks to @BionicCode, I found a solution.

You can get visual DataGridRow:

DataGridRow row = (DataGridRow)datagrid.ItemContainerGenerator.ContainerFromItem(item);

and than you can check for errors:

if (Validation.GetHasError(row))

and also you have access to row.BindingGroup, which contains all the bindings in this row (.BindingExpressions), and many other information (IsDirty, ValidationErrors, ValidationRules, CancelEdit())

But, when you want to check for errors in cells, it's not that easy. Unfortunately, DataGridCell doesn't contain information about errors, Validation.GetHasError(cell) not working. You need to look deeper into the visual tree.

private void CancelChangesCellsHavingError()
{
    SomethingItem item = datagrid.CurrentItem as SomethingItem;

    DataGridRow row = (DataGridRow)datagrid.ItemContainerGenerator.ContainerFromItem(item);

    if (Validation.GetHasError(row))
    {
        var cc = dataGrid.CurrentCell;
        foreach (DataGridColumn col in datagrid.Columns)
        {
            DataGridCell cell = (DataGridCell)col.GetCellContent(item).Parent;
            List<DependencyObject> errs = GetVisualChildrenHavingError(cell);
            if (errs != null)
            {
                datagrid.CurrentCell = new DataGridCellInfo(item, col);
                datagrid.CancelEdit(DataGridEditingUnit.Cell);
            }
        }
        dataGrid.CurrentCell = cc;
    }
}

/// <summary>
/// Returns all visual children that HasError. Return null if nothing is found.
/// </summary>
public static List<DependencyObject> GetVisualChildrenHavingError(DependencyObject parent)
{
    List<DependencyObject> result = null;
    GetVisualChildrenHavingError(parent, ref result);
    return result;
}

private static void GetVisualChildrenHavingError(DependencyObject parent, ref List<DependencyObject> result)
{
    for (int childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
    {
        DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);

        if (Validation.GetHasError(childElement))
        {
            if (result == null)
                result = new List<DependencyObject>();

            result.Add(childElement);
        }

        GetVisualChildrenHavingError(childElement, ref result);
    }
}

Useful links - binding, validation, DataGrid: