Berryl
Berryl

Reputation: 12833

Canceling the Add of a new row in a DataGrid

I have an ObservableCollection that is bound to a DataGrid where I want the User to be able to add data into the grid, but ONLY while the sum of all entries added is less than 100%.

To get the data grid to focus the cells that accept data, I'm using code behind that handle the DataGrid.RowEditEnding event. It's tricky but it works, to a point.

The point being where we have entries totaling 100%. I can catch the add I don't want in the CollectionChanged event handler, but of course I can't alter the collection once I'm in there.

Anyone got an suggestion as to a good place to catch and deal with an unwanted add?

Cheers,
Berryl

ViewModel event handler code

public ObservableCollection<RatioBagEntryVm> Ratios { get; private set; }

public RatioBagAllocatorVm() {
    RatioBag = new RatioBag();
    Ratios = new ObservableCollection<RatioBagEntryVm>();
    Ratios.CollectionChanged += OnRatiosChanged;
}

private void OnRatiosChanged(object sender, NotifyCollectionChangedEventArgs e) {
    SequencingService.Sequence(Ratios);

    switch (e.Action)
    {
        case NotifyCollectionChangedAction.Add:
            foreach (var entryVm in e.NewItems.Cast<RatioBagEntryVm>()) {
                entryVm.PropertyChanged += OnRatioEntryChanged;
                RatioBag.Add(entryVm.Ratio);
                entryVm.Bag = RatioBag;
            }
            break;
        case NotifyCollectionChangedAction.Remove:
            foreach (var entryVm in e.OldItems.Cast<RatioBagEntryVm>()) {
                RatioBag.RemoveAt(entryVm.SequenceNumber - 1);
            }
            break;
        default:
            throw new ArgumentOutOfRangeException();
    }
}

Code Behind handler

    /// <summary>
    /// Adapted from http://blogs.msdn.com/b/vinsibal/archive/2009/04/14/5-more-random-gotchas-with-the-wpf-datagrid.aspx
    /// </summary>
    private void OnDataGridRowEditEnding(object sender, DataGridRowEditEndingEventArgs e)
    {
        var dg = sender as DataGrid;
        if (e.EditAction != DataGridEditAction.Commit) return;

        //
        // custom commit action:
        // moves to the next row and opens the second cell for edit
        // if the next row is the NewItemPlaceholder
        //

        var wasLastRowInGrid = e.Row.Item == dg.Items[dg.Items.Count - 2];
        if (!wasLastRowInGrid) return;
        if (dg.HasError()) return;


        // set the new cell to be the last row and the second column
        const int colIndex = 1;
        var rowToSelect = dg.Items[dg.Items.Count - 1];
        var colToSelect = dg.Columns[colIndex];
        var rowIndex = dg.Items.IndexOf(rowToSelect);

        switch (dg.SelectionUnit)
        {
            case DataGridSelectionUnit.Cell:
            case DataGridSelectionUnit.CellOrRowHeader:
                // select the new cell
                dg.SelectedCells.Clear();
                dg.SelectedCells.Add(new DataGridCellInfo(rowToSelect, colToSelect));
                break;
            case DataGridSelectionUnit.FullRow:
                e.Row.IsSelected = true;
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }

        // this is the extra tricky part
        Dispatcher.BeginInvoke(new DispatcherOperationCallback(param =>
        {
            // get the new cell, set focus, then open for edit
            var cell = dg.GetCell(rowIndex, colIndex);
            cell.Focus();

            dg.BeginEdit();
            return null;
        }), DispatcherPriority.Background, new object[] { null });
    }
}

SOLUTION (for now)

The tricky part of the original code was using the Dispatcher to mimic what you'd like to have available in a DataGrid.RowEndedEvent, as per Vincent Sibal who wrote the idea I based my code on.

So, this is the spot to cancel also, and the modified code is below. Accessing the view model this way is hardly the stuff to read about in MVVM digest, and is certainly a hack of a hack, but... it works.

        // this is the extra tricky part
        Dispatcher.BeginInvoke(new DispatcherOperationCallback(param =>
        {
            // get the new cell, set focus, then open for edit
            var cell = dg.GetCell(rowIndex, colIndex);
            cell.Focus();

            // cancel the row commit if we are already fully allocated
            var win = dg.FindVisualParent<Window>();
            var vm = win.DataContext as RatioBagAllocatorVm;
            if(vm.RatioBag.IsAllocatable) 
                e.Cancel = true;
            else {
                dg.BeginEdit();
            }
            return null;
        }), DispatcherPriority.Background, new object[] { null });

Upvotes: 2

Views: 4396

Answers (1)

ianschol
ianschol

Reputation: 686

Edit : Leaving the below post for context, although it's much cleaner to CANCEL the event, as indicated in the updated question:

e.Cancel = true;

...

Why can't you remove the unwanted add? Something like this should work (pardon any syntax errors!) :

private bool revertActive = false;
private void OnRatiosChanged(object sender, NotifyCollectionChangedEventArgs e)
{
   if (revertActive) return;

   if (e.Action == NotifyCollectionChangedAction.Add)
   {
      foreach (var entryVm in e.NewItems.Cast<RatioBagEntryVm>()) 
      {
          entryVm.PropertyChanged += OnRatioEntryChanged;
          RatioBag.Add(entryVm.Ratio);
          entryVm.Bag = RatioBag;
      }

      if (entryVm.Ratio > 100)
      {
          revertActive = true;
          foreach(object newItem in e.NewItems)
          {
              (sender as ObservableCollection).RemoveItem(newItem);
          }
          revertActive = false;

          //...Any other revert code here...
      }
   }
}

That said, it's less clunky to prevent the items from ever being added in the first place, rather than removing them after the fact.

Upvotes: 1

Related Questions