Reputation: 12833
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
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();
}
}
/// <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 });
}
}
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
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