Silverlight NumericUpDown inside Datagrid strange behaviour [duplicate] - silverlight

I have a DataGrid. One of the columns is a template with a CheckBox in it. When the Checked or Unchecked events trigger, (it happens with both) the CheckBox's DataContext is sometimes null, which causes my code to error. It seems to be null most often if the mouse is moving while you press and release the button quickly (it's intermittent).
I listened for changes to the DataContext of the CheckBox by making views:ListenCheckBox (extends CheckBox) and attaching a binding, and it's never set to null, but it is set from null to a Task at times I wouldn't expect, i.e. after the DataGrid has been totally generated and you're checking/unchecking boxes. Immediately after the [un]checked event runs with a null DataContext, I get the notification that shows the DataContext changed from null to a Task, so it appears that when I get a null DataContext, it's because it hadn't actually set the DataContext by the time it ran the Checked/Unchecked event.
Also, I added Tag="{Binding}" to the CheckBox for debugging. The Tag is not null (i.e. it has the proper object) more often than the DataContext, but still not all the time.
Here are the relevant bits of the XAML code:
<navigation:Page.Resources>
<sdk:DataGridTemplateColumn x:Key="DeleteOrPrintSelect" Header="Delete Or Print Notes Selection">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<views:ListenCheckBox IsChecked="{Binding DeleteOrPrintNotesSelection, Mode=TwoWay}" Checked="DeletePrintNotesCheckBox_Changed" Unchecked="DeletePrintNotesCheckBox_Changed" HorizontalAlignment="Center" VerticalAlignment="Center" Tag="{Binding}" />
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
</sdk:DataGridTemplateColumn>
</navigation:Page.Resources>
<sdk:DataGrid x:Name="dataGrid1" Grid.Column="1" Grid.Row="2" AutoGeneratingColumn="dataGrid1_AutoGeneratingColumn">
<sdk:DataGrid.RowGroupHeaderStyles>
[removed]
</sdk:DataGrid.RowGroupHeaderStyles>
</sdk:DataGrid>
And the relevant code behind:
// Create a collection to store task data.
ObservableCollection<Task> taskList = new ObservableCollection<Task>();
[code adding Tasks to taskList removed]
PagedCollectionView panelListView = new PagedCollectionView(taskList);
this.dataGrid1.ItemsSource = panelListView;
}
private void dataGrid1_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (e.PropertyName == "DeleteOrPrintNotesSelection")
{
e.Column = Resources["DeleteOrPrintSelect"] as DataGridTemplateColumn;
}
else
{
e.Column.IsReadOnly = true;
}
}
private void DeletePrintNotesCheckBox_Changed(object sender, RoutedEventArgs e)
{
try
{
var cb = sender as CheckBox;
var t = cb.DataContext as Task;
t.DeleteOrPrintNotesSelection = cb.IsChecked == true;
PagedCollectionView pcv = this.dataGrid1.ItemsSource as PagedCollectionView;
ObservableCollection<Task> taskList = pcv.SourceCollection as ObservableCollection<Task>;
bool anySelected = taskList.Any(x => x.DeleteOrPrintNotesSelection);
this.btnPrint.IsEnabled = anySelected;
this.btnDelete.IsEnabled = anySelected;
}
catch (Exception ex)
{
ErrorMessageBox.Show("recheck", ex, this);
}
}
Any ideas? Thanks in advance.

I found that the problem happened when you double click on the cell and it moved it to the cell editing template. In my case, I didn't have a cell editing template defined, so it used the same cell template, but instead of not changing anything, it apparently decided to make a new check box. I set the column's IsReadOnly property to true, and it fixed it. An alternate solution:
DataContext="{Binding}" (in XAML, or the code equivalent:)
cb.SetBinding(FrameworkElement.DataContextProperty, new Binding());
I'm not sure why this one fixes it, since I thought the default DataContext is {Binding}. Perhaps it's a Silverlight bug, and it gets set in a different order if you define it explicitly instead of leaving it the default.

Related

WPF MVVM: Strange Binding behavior

I have a UserControl that contains a TabControl.
<UserControl x:Class="Test.MyUC"
....
xmlns:vm="clr-namespace:Test.ViewModels"
xmlns:ikriv="clr-namespace:IKriv.Windows.Controls.Behaviors"
...
<UserControl.Resources>
<vm:MyUCVM x:Key="VM" />
</UserControl.Resources>
<UserControl.DataContext>
<StaticResourceExtension ResourceKey="VM" />
</UserControl.DataContext>
<!-- Using Ivan Krivyakov's Attached Behavior -->
<TabControl ikriv:TabContent.IsCached="True"
TabStripPlacement="Top" ItemsSource="{Binding TabList}" IsSynchronizedWithCurrentItem="True">
<TabControl.Resources>
<DataTemplate DataType="{x:Type vm:MyTab1VM}">
<v:MyTab1/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:MyTab2VM}">
<v:MyTab2/>
</DataTemplate>
</TabControl.Resources>
...
Of course, in MyUCVM, I have TabList. Now, up to this point, everything works fine.
The problem starts when one of the tabs (e.g. MyTab1) in the TabControl needs to continuously and recursively read data from some external source (done in the ViewModel of course), and pass that data to View (via Binding) to display. Even up to this point everything is working. However, I do not want that to run when the tab is not visible, because there is no point to do that.
To do that, MyTab1VM needs to know if the associated View (MyTab1) is the selected tab. Therefore, I wired this up:
MyTab1:
<Style TargetType="TabItem">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=OneWayToSource}" />
</Style>
MyTab1VM
public static readonly DependencyProperty IsSelectedProperty =
DependencyProperty.Register("IsSelected",
typeof(bool),
typeof(MyTab1VM),
new PropertyMetadata(false, new PropertyChangedCallback(IsSelectedChanged))
);
public bool IsSelected
{
get
{
return (bool) GetValue(IsSelectedProperty);
}
set
{
SetValue(IsSelectedProperty, value);
}
}
public static void IsSelectedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.Property == IsSelectedProperty)
{
MyTab1VM vm = d as MyTab1VM ;
vm.SetupToGetData();
}
}
private void SetupToGetData()
{
if (this.IsSelected)
{
System.Windows.Threading.DispatcherTimer timer = new System.Windows.Threading.DispatcherTimer();
timer.Interval = TimeSpan.FromMilliseconds(100);
timer.Tick += timer_Tick;
timer.Start();
}
}
private void timer_Tick(object sender, EventArgs e)
{
if (this.IsSelected)
this.MyData = ExternalSource.GetData();
else
{
(sender as System.Windows.Threading.DispatcherTimer).Stop();
}
}
Unfortunately, this setup only works when I set this.IsSelected = true; manually in the MyTab1VM's constructor. Leaving that out in the constructor, the data do not get shown in the view.
I have set breakpoints and confirmed that the binding for IsSelected is running correctly. Even the timer is running, and ExternalSource.GetData() is being called. But this.MyData = ExternalSource.GetData(); is not triggering the change from the ViewModel to the View.
The most puzzling part is that the same binding is triggered if IsSelected is set to true from the constructor.
Anyone out there knows what happened here?
I managed to do some fruitful troubleshooting on my own. I made a breakpoint in SetupToGetData() and I put this.GetHashCode() in my debugging watchlist. When I manually set this.IsSelected = true in the constructor, I realized that the SetupToGetData() method is called twice, with two different hash values. Planting another breakpoint in the constructor also showed that the constructor is called when I switch to this tab.
I have decided to move this to a new question, because it looks highly possible that the problem has nothing to do with binding.
Edit
Seems like I was right that this is the root of this problem. As that question is solved, so is this as well.

Populate a second DataGrid once I double click on a row WPF

I am still a little new to wpf and MVVM. I am trying to code a solution without breaking that pattern. I have two (well three, but for the scope of this question just two) DataGrids. I want to double click on the row of one, and from that load data Into the second DataGrid (ideally I would spin up a second thread that would load the data). So far I can get a window to pop up when I double click on a row. I throw the code for the event into the code behind for the xaml. To me that seems very windows formish. Somehow or the other I feel like that breaks the pattern a great deal.
private void DataGrid_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e) {
if (popDataGrid.SelectedItem == null) {
return;
}
var selectedPopulation = popDataGrid.SelectedItem as PopulationModel;
MessageBox.Show(string.Format("The Population you double clicked on has this ID - {0}, Name - {1}, and Description {2}", selectedPopulation.populationID, selectedPopulation.PopName, selectedPopulation.description));
}
That is the code for the event in the code behind and here is the grids definition in the xaml:
<DataGrid ItemsSource="{Binding PopulationCollection}" Name="popDataGrid"
AutoGenerateColumns="False" RowDetailsVisibilityMode="VisibleWhenSelected"
CanUserAddRows="False" Margin="296,120,0,587" HorizontalAlignment="Left" Width="503" Grid.Column="1"
MouseDoubleClick="DataGrid_MouseDoubleClick">
</DataGrid>
I am thinking this code should go in the MainWindowViewModel. So I am attempting to create a command:
public ICommand DoubleClickPopRow { get { return new DelegateCommand(OnDoubleClickPopRow); }}
and the same event handler:
private void OnDoubleClickPopRow(object sender, MouseButtonEventArgs e) {
}
But the ICommand is throwing an exception when it returns the DelegateCommand(OnDoubleClickPopRow).
Well, one can plainly see that the number of arguments doesn't match. I know I am doing something wrong, but I am not quite sure what it is. I will continue to research this but any help you guys can give would be greatly appreciated.
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
<DataGrid ItemsSource="{Binding PopulationCollection}" Name="popDataGrid"
AutoGenerateColumns="False" RowDetailsVisibilityMode="VisibleWhenSelected"
CanUserAddRows="False" Margin="296,120,0,587" HorizontalAlignment="Left" Width="503" Grid.Column="1" SelectedItem="{Binding ItemInViewModel}"></DataGrid>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<i:InvokeCommandAction Command="{Binding Save_Bid}" />
</i:EventTrigger>
</i:Interaction.Triggers>
You can add this to your DataGrid and add your code in your viewmodel.
Now that we have a selected item bound to an item in our view model we can use that item to know when we can fire the even we want as well as what item to use when the event is fired When the event can be fired
bool Can_Fire_Event()
{
if(ItemInViewModel != null)
{ return true; } else { return false; }
}
private RelayCommand _saveBid;
public ICommand SaveBid
{
get
{
if (_saveBid == null)
{
_saveBid = new RelayCommand(param => Save_Bid(), param => Can_Fire_Event());
}
return _saveBid;
}
}
public void Save_Bid()
{
//Open your new Window here, using your "ItemInViewModel" because this event couldn't be fired from your datagrid unless the "ItemInViewModel" had a value assigned to it
}

Why does the ComboBox in my DataGrid column reset itself to null?

In my WPF application, I am developing a fairly straightforward page that allows either creating a new object or choosing one from a combo box, then editing the object.
One of the parts of the object that is editable is a related database table in a one-to-many relationship, so for that piece I used a DataGrid. The DataGrid itself has a data-bound ComboBox column, as you can see here:
<DataGrid AutoGenerateColumns="False" EnableRowVirtualization="True"
CanUserAddRows="False" CanUserDeleteRows="True"
ItemsSource="{Binding Path=No.Lower_Assy}"
DataGridCell.Selected="dgAssy_GotFocus">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Number & Type">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Path=DataContext.ComboSource, RelativeSource={RelativeSource AncestorType=Page}}"
SelectedValuePath="bwk_No"
SelectedValue="{Binding Path=fwf_Higher_N, ValidatesOnDataErrors=True, ValidatesOnExceptions=True, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Number}"/>
<TextBlock Text="{Binding Path=Type}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- other text columns omitted -->
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="Delete" Click="btnDeleteHigherAssy_Click" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
Code behind:
private void dgAssy_GotFocus(object sender, RoutedEventArgs e)
{
if (e.OriginalSource.GetType() == typeof(DataGridCell))
{
// Starts the edit on the row
DataGrid grd = (DataGrid)sender;
grd.BeginEdit(e);
}
}
And for the save button:
private void btnSave_Click(object sender, RoutedEventArgs e)
{
if (CanUserEdit())
{
if (string.IsNullOrWhiteSpace(model.Data.Error))
{
repo.Save(model.Data);
StatusText = STATUS_SAVED;
model.CanSave = false;
// This is the data source for the main combo box on the page
model.ComboSource = repo.GetData();
// Set the combo box's selected item, in case this is a new object.
// cboNo is the main combo box on the page which allows selecting
// an object to edit
// Apparently setting SelectedItem directly doesn't work on a databound combo box
int index = model.ComboSource.ToList().FindIndex(x => x.bwk_No == model.Data.bwk_No);
cboNo.SelectedIndex = index;
}
else
{
MessageBox.Show("Invalid data:\n" + model.Data.Error, "Cannot save");
}
}
}
The problem
When I choose an item from the combo box in the data grid, it seems to work until I click on the save button. Then two things happen:
The combo box's selected item is set to null, blanking out the combo box.
As a result of (1), the save button is re-enabled because the data has changed. (The save button is bound to model.CanSave, which as you can see is set to false in the button handler; it is set to true by a property change event handler if there are no data errors.)
Why is it being reset? I've followed the code flow closely and can see the property change event for the combo box's backing field (fwf_Higher_N) being handled, and it appears to somehow come from the line model.ComboSource = repo.GetData();, but the stack only shows [external code] and I don't see why that line would modify an existing object.
The model class
// Names have been changed to protect the innocent
private class MyDataViewModel : INotifyPropertyChanged
{
private DbData _Data;
public DbData Data
{
get { return _Data; }
set
{
_Data = value;
OnPropertyChanged("Data");
}
}
private IQueryable<MyComboModel> _ComboSource;
public IQueryable<MyComboModel> ComboSource {
get { return _ComboSource; }
set
{
_ComboSource = value;
OnPropertyChanged("ComboSource");
}
}
private bool _CanSave;
public bool CanSave
{
get { return _CanSave; }
set
{
_CanSave = value;
OnPropertyChanged("CanSave");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
Your description of what is going on and your markup doesn't quite match. I'm going to make some assumptions, such as that Page.DataContext is an instance of MyDataViewModel.
I'm sorry to say it, but a SSCCE would do wonders here. I strongly suggest when anyone gets into situations where they are elbow deep in code they don't quite understand that they break out what they are attempting to do and create a minimal prototype that either exhibits the same behavior, or that helps you learn what's going wrong. I've made 500+ prototypes in the past five years.
As for this situation, you refer to a ComboBox named cboNo in btnSave_Click, but I don't see that in the xaml. This ComboBox's ItemSource appears to be bound to MyDataViewModel.ComboSource.
In addition, all ComboBoxes in the DataGrid also appear to be bound to the model's ComboSource. And, in the button handler event, you change what is in the property:
// This is the data source for the main combo box on the page
model.ComboSource = repo.GetData();
This fires PropertyChanged, and every ComboBox bound to this property will be updated. That means not only cboNo but also every ComboBox in the DataGrid.
It is expected behavior that, when ComboBox.ItemsSource changes, if ComboBox.SelectedItem is not contained within the items source, that SelectedItem is nulled out.
I just spun up a prototype (501+) and it appears that if the IEnumerable that the ComboBox is bound to changes, but the elements in the IEnumerable do not, then SelectedItem is not nulled out.
var temp = combo.ItemsSource.OfType<object>().ToArray();
combo.ItemsSource = temp;
So, within the btnSave_Click event handler, you change this ItemsSource, which probably does not have the same instances that are already in the combo, thus nulling out SelectedItem for all ComboBoxes bound to this property, and then only update cboNo's SelectedIndex.
Now, as for what to do about it...
Well, not sure. From the rest of your code, it appears you need to do some more codebehind work to make sure only the necessary ComboBoxes have their sources updated...

Databinding not updating ListBox completely

I'm using the MVVM pattern and have a databound Listbox that isn't updating completely.
There is a modelview that contains an Observable collection of Machines which is bound to the list:
<ListBox Name="MachinesList"
Height="300"
Width="290"
DataContext="{Binding Path=AllMachines}"
SelectionMode="Single"
ItemsSource="{Binding}" SelectionChanged="MachinesList_SelectionChanged"
HorizontalAlignment="Right"
>
The collection AllMachines Contains an observable collection of MachineModelViews which are in turn bound to a MachineView that presents the name and location of the machine:
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Label Name="NameLabel" Content="{Binding Path=Name}" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Width="50" />
<Label Content="Location:" Width="120"
HorizontalAlignment="Right"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Target="{Binding ElementName=locationLabel}"
/>
<Label Content="{Binding Path=Location.Name}" Name="locationLabel" HorizontalContentAlignment="Center" Width="60"/>
</StackPanel>
The problem:
When values are added to the collection things update okay. When a machine is deleted however only the Label bound to Location.Name updates the other two remain in the listbox. I've confirmed that the collection is actually updating and removing the MachineModelView Correctly but some how the label with it's name and the "label label" with "Location:" continues to exist until the application is restarted:
before:
after delete:
after app restart:
The delete button calls a command which removes the item from the private member that backs the AllMachines property and from the repository (which ultimately plugs into a database via Entity Framework):
RelayCommand _deleteCommand;
public ICommand DeleteCommand
{
get
{
if (_deleteCommand == null)
{
_deleteCommand = new RelayCommand(
param => this.Delete(),
null
);
}
return _deleteCommand;
}
}
void Delete()
{
if (_selectedMachine != null && _machineRepository.GetMachines().
Where(i => i.Name == _selectedMachine.Name).Count() > 0)
{
_machineRepository.RemoveMachine(_machineRepository.GetMachines().
Where(i => i.Name == _selectedMachine.Name).First());
_allMachines.Remove(_selectedMachine);
}
}
Note: There can only be one item with a name in AllMachines (this is handled by the add logic in the repository and command itself) so removing the "First" one should be fine.
The AllMachines property:
public ObservableCollection<MachineViewModel> AllMachines
{
get
{
if(_allMachines == null)
{
List<MachineViewModel> all = (from mach in _machineRepository.GetMachines()
select new MachineViewModel(mach, _machineRepository)).ToList();
_allMachines = new ObservableCollection<MachineViewModel>(all);
}
return _allMachines;
}
private set
{
_allMachines = value;
}
}
The selection changed event handler:
private void MachinesList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0 && e.AddedItems[0] is MachineViewModel)
((MachinesViewModel)this.DataContext).SelectedMachine = (MachineViewModel)e.AddedItems[0];
}
If 'AllMachines is your actual observable collection, I would bind the itemssource to that instead of binding it to ur datacontext, and then bind ur viewmodel as your datacontext. It may also be helpful to call the updatesourcetrigger after your itemssource bind.
DataContext="{Binding Path=YourViewModel}"
SelectionMode="Single"
ItemsSource="{Binding AllMachines, UpdateSourceTrigger="PropertyChanged"}"
And you can use that updatesourcetrigger on some of your labels as well where you are binding. Other than that, you xaml looks ok, hard for anyone to really say until they see everything.
_machineRepository.RemoveMachine(_machineRepository.GetMachines().
Where(i => i.Name == _selectedMachine.Name).First());
AllMachines.Remove(_selectedMachine);
Instead of field _allMachines.Remove remove directly from the property AllMachines like
AllMachines.Remove
I hope this will help.
if you code the way you said here, then all should work. pls post the missing code pieces:
your AllMaschines Property
your MachinesList_SelectionChanged event <-- when doing MVVM you almost not need this
pls debug your Delete() method just to be sure that: _allMachines.Remove(_selectedMachine); is hit and the machine is really removed from your collection.
I found my problem, really stupid: I have an event that fires when the repository updates. Before I only had an add command and I was adding the ability to delete when this problem came up. Turns out the event was being handled by the MachinesModelView to update it's internal variable that is the source for the AllMachines property:
void OnMachineAddedToRepository(object sender, MachineAddedOrRemovedEventArgs e)
{
var viewModel = new MachineViewModel(e.NewMachine, _machineRepository);
this.AllMachines.Add(viewModel);
}
I modified the event arguments to toggle between add and delete but forgot to update the event handler. Now it does work:
void OnMachineAddedOrRemovedFromRepository(object sender, MachineAddedOrRemovedEventArgs e)
{
if (e.Added)
{
var viewModel = new MachineViewModel(e.NewMachine, _machineRepository);
this.AllMachines.Add(viewModel);
}
else if (AllMachines.Where(i => i.Name == e.NewMachine.Name).Count() > 0)
AllMachines.Remove(AllMachines.Where(i => i.Name == e.NewMachine.Name).First());
}
What made this so hard to track down is that the extra item only lived very briefly until the AllMachines.Remove part of the Delete command ran. So checking the pre-delete count, and the post delete count looked right it was the guts in the middle when the item is being deleted from the repository that fired off an event that added the item back and left it in this weird state. Kind of odd that it consistently only had the location part of the MachineModelView floating around. I now just let the event handler add or remove the items from the _allMachines variable and don't touch it explicitly in the delete command (the delete command just deletes it from the repository and lets the UI variables catch up in the fraction of a second it takes for the event to trigger). Thanks a bunch guys for your help.

DataGrid Looses Focus When Delete Key is Pressed

I'm doing MVVM where a DataGrid is bound to an ObservableCollection with a DeleteItemCommand hooked up to the DataGrid.InputBindings as follows:
<DataGrid.InputBindings>
<KeyBinding Key="Delete" Command="{Binding DeleteItemCommand}" />
</DataGrid.InputBindings>
The item and row are removed when the user hits the delete key but the grid looses focus. You have to click or tab to the grid so it regains focus before hitting Delete to remove another row (pretty freaking annoying). I tried setting the DataGrid.CanUserDeleteRows="False" but it doesn't make any difference.
I replaced the DataGrid with a ListView and the ListView retains focus.
Is this a bug with the DataGrid or am I doing something wrong? Peace and love, peace and love!
I solved this by using the built in functionality of WPF DataGrid. The grid handles removing items by default if the underlying collection is editable (if the collection is dedicated to this purpose that's no problem, otherwise an intermediate collection can be added...). I avoided any key bindings and just set up the grid like this:
<DataGrid ItemsSource="{Binding InvoiceItems}" IsReadOnly="False" CanUserDeleteRows="True" CanUserAddRows="False">
The ItemsSource collection is of type BidningCollection<>
In my ViewModel (my DataContext) I add a handler for CollectionChanged event:
InvoiceItems.CollectionChanged += InvoiceItemsCollectionChanged;
And implement it like this:
private void InvoiceItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action != NotifyCollectionChangedAction.Remove)
return;
foreach (var oldItem in e.OldItems)
{
//do any other processing necessary
}
}
That's because you will probably be having at least two ways of removing an item from you underlying collection (keyboard with Del key, some button) and maybe some other things to take care of when an item is deleted.
I bumped on that some time ago. Somehow this event is never raised.
Try
this approach.
Long story short, event PreviewKeyDown will get you where you want.
And in MVVM-friendly manner:
<i:Interaction.Triggers>
<i:EventTrigger EventName="PreviewKeyDown">
<i:InvokeCommandAction Command="{Binding DeleteItemCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
Did you check this answer yet?
How to bind delete action (in WPF Datagrid) to a command or property in view model
You probably need to:
make sure CanUserDeleteRows="False"
make sure the key is actually bound to the specified command in your datacontext, like this:
<DataGrid.InputBindings>
<KeyBinding Key="Delete" Command="{Binding DataContext.DeleteEntry, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGrid}}"/>
</DataGrid.InputBindings>
Previously I fail at item #2 and wrote Command="{Binding DeleteEntry}", while in fact I should bind to DataContext.DeleteEntry by RelativeSource.
I had to solve the issue in a different way than the jl.'s answer, because it could not do any processing before (e.g. checking access) deletion happen as a Command could. While perhaps not as robust, it does exactly what you asked about. Keeping your original code unchanged, just hook the following SelectionChanged, or even better use attached property.
Because deleting an item first produces SelectionChanged with index -1 it is easy enough to reliably guess when deletion happen and set a flag. After the first invocation with -1 another one with nearest neighbor index happens, at this point if the flag was set, it is safe to focus the current cell:
private int LastItemCount = 0;
private bool ShouldFocusOnSelection = false;
private void FocusOnDeleteDG_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is DataGrid dg)
{
if (IsRemovalEvent(dg, e))
{
ShouldFocusOnSelection = true;
}
else if (ShouldFocusOnSelection)
{
dg.FocusCurrentCell();
ShouldFocusOnSelection = false;
}
LastItemCount = dg.Items.Count;
}
}
where IsRemovalEvent checks if the selection event was produced by item removal:
private static bool IsRemovalEvent(DataGrid dg, SelectionChangedEventArgs e)
{
return e.RemovedItems.Count > 0
&& e.AddedItems.Count == 0
&& dg.SelectedIndex == -1
&& dg.Items.Count > 0
&& LastItemCount > dg.Items.Count;
}
and the FocusCurrentCell/GetChildren are helper methods you probably already have:
public static void FocusCurrentCell(this DataGrid dataGrid)
{
var rowIndex = dataGrid.SelectedIndex != -1 ? dataGrid.SelectedIndex : (dataGrid.Items.Count > 0 ? 0 : -1);
if (!(dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndex) is DataGridRow row))
{
return;
}
if (dataGrid.CurrentColumn?.DisplayIndex != null)
{
// traverse VisualTree using VisualTreeHelper.GetChild()
var cell = row.GetChildren<DataGridCell>()
.Skip(dataGrid.CurrentColumn.DisplayIndex).FirstOrDefault();
Keyboard.Focus(cell);
}
}

Resources