I'm having trouble working with the List View setup to Bind Multiple Selections and remember the selections when I switch the Selection Mode to between Single and Multiple.
Context
I'm creating a Soccer coaching app. The List View will display the Player Roster. On game day, I want an easy way to select the players that showed up to play. I would like to use the Multiple Selection mode to allow the user to select the players that showed up. I will provide a button in the app bar that will control the SelectionMode through a converter. It will switch the List View from Single to Multiple. This part works fine. I can see the List View switch between Single and Multiple.
The part that doesn't work fine is the binding of Selected Items. I must be missing something because it seems extremely difficult to bind to the SelectedItems property reliably. The best thing that has worked so far is the ListViewExtentions from the WinRTXamlToolkit which is what I show in my xaml below. It seems to bind the items to the backing property in the view mode when the selection is made, however, when the SelectionMode is switched back to Single, the back property is cleared out. Also, without modifying the extension code, it broke my SelectionMode code. There is a castastrophic failure in the extension on _listView.SelectedItems.Clear(). If I remove that line, then SelectionMode is back working.
I don't care to use the ListViewExtentions from WinRTXamlToolkit. I only present it here so you know that I have tried it. Ultimately, I'm looking for the right solution for binding SelectedItems.
Here is the List View XAML.
<ListView ItemsSource="{Binding Roster}"
toolkitExt:ListViewExtensions.BindableSelection="{Binding SelectedPlayers, Mode=TwoWay}"
SelectionMode="{Binding IsEditingGameRoster, Converter={StaticResource ListViewSelectionModeFromBooleanNoneOrMultipleConverter}}">
<ListView.ItemTemplate>
<DataTemplate>
<!-- List View Display Not important for describing problem. -->
</DataTemplate>
</ListView.ItemTemplate>
Namespace is: xmlns:toolkitExt="using:WinRTXamlToolkit.Controls.Extensions"
The View Model has the properties:
Roster : ObservableCollection
IsEditingGameRoster : Bool
SelectedPlayers : ObservableCollection
Thanks for any samples that demonstrate binding Multiple Selection especially if it handles switching SelectionMode as well.
Thanks,
Tom
I was facing this problem too. When i have trying to change Selection mode in converter it worked not as expected. Any workaround i have found is to use multiple selection mode, but operate in CollectionChanged event with added/removed items. For example if i need Single selection mode i am rewriting selection to new selected item.
Something like this:
private void OnSelectedPlayersChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
if (this.SelectionMode == Mode.Once)
{
SynchronizationContextProvider.UIThreadSyncContext.Post((d) =>
{
this.SelectedPlayers.Clear();
this.SelectedPlayers.Add((Players)e.NewItems[0]);
}, null);
}
}
if (e.Action == NotifyCollectionChangedAction.Remove)
{
//some logic
}
}
It is the best option, what i have found so far. If you'll figure out something better i'll happy to use it.
I ended up solving my issue close to what I would like to see but still not perfect. I ended up creating a sub-class of ListView and adding two properties SelectedItems2 and SelectionMode2. If anyone could help adjust the following code so that I could reuse SelectedItems and SelectionMode that would make the solution much closer to perfect.
Sub-Class Code
/// <summary>
/// Enhanced List View
///
/// SeletionMode Switching:
/// This class enhances ListView to support changing the selection mode from None/Single to Multiple and back and having it
/// bind the selected items without forgetting them when changing selection mode. The base ListView will remove the selected items when switching
/// from Multiple SelectionMode to another SelectionMode.
/// 1) Created SelectedItems2 to bind to View Model as SelectedItems will have items removed and SelectedItems is not easy to bind to SelectedItems.
/// 2) On SelectionChanged, remember the selected items in SelectedItems2 how the user selected or unselected them.
/// 3) On SelectionMode set to Multiple, fill the underlying SelectedItems with the values from SelectedItems2 so that the checkboxes are checked on the ListView.
/// 4) Use _isMergingItems flag to ignore selection changed events being fired by merging of the SelectedItem2 and SelectedItems2 collections.
///
/// Doesn't support changing SelectedItems2 bound collection while in Multiple SelectionMode yet.
/// To do so we would need to hook the CollectionChanged event on the SelectedItems2 List object that is bound.
/// and likely handle threading issues with the merging of SelectedItems and SelectedItems2.
/// </summary>
public class MyListView : ListView
{
private bool _isMergingItems;
public MyListView()
{
SelectionChanged += MyListView_SelectionChanged;
}
private void MyListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// Ignore selection changed event when selection mode is not multiple.
// This is mainly here because when changing from Multiple to Single, this event is fired and will cause
// the items to be removed from SelectedItems2 which is not what is desired.
if (SelectionMode != ListViewSelectionMode.Multiple) { return; }
if (_isMergingItems) { return; }
try
{
_isMergingItems = true;
var selectedItems2List = SelectedItems2 as IList;
if (selectedItems2List == null) { return; }
foreach (var itemToAdd in e.AddedItems)
{
selectedItems2List.Add(itemToAdd);
}
foreach (var itemToRemove in e.RemovedItems)
{
selectedItems2List.Remove(itemToRemove);
}
}
finally
{
_isMergingItems = false;
}
}
#region --- Dependency Properties ---
private static readonly DependencyProperty SelectionMode2Property = DependencyProperty.Register("SelectionMode2", typeof(ListViewSelectionMode), typeof(MyListView), new PropertyMetadata(ListViewSelectionMode.None, SelectionMode2Changed));
public ListViewSelectionMode SelectionMode2
{
get { return (ListViewSelectionMode)GetValue(SelectionMode2Property); }
set { SetValue(SelectionMode2Property, value); }
}
private static void SelectionMode2Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var listView = d as MyListView;
var value = (ListViewSelectionMode)e.NewValue;
// Set the underlying selection mode property to cause the control to
// change to and from Multiple selection mode.
listView.SelectionMode = value;
// When Multiple selction mode, we will need to load the underlying SelectedItems of the ListView
// so that the checkboxes are checked appropriatly. When the selection mode is changed the underlying ListView
// removes the selected items.
if (value == ListViewSelectionMode.Multiple)
{
listView.FillUnderlyingSelectedItems();
}
}
private static readonly DependencyProperty SelectedItems2Property = DependencyProperty.Register("SelectedItems2", typeof(object), typeof(MyListView), null);
/// <summary>
/// SelectedItems2 is used as the property to bind the selected items to the ViewModel.
/// Using a List as the data type did not seem to work for binding. Since ItemSource is an object on the base class
/// object was chosen for this property as well.
///
/// However, SelectedItems2 will need to bind to IList in order for it to work properly.
/// </summary>
public object SelectedItems2
{
get { return (object)GetValue(SelectedItems2Property); }
set { SetValue(SelectedItems2Property, value); }
}
#endregion
private void FillUnderlyingSelectedItems()
{
var selectedItems2List = SelectedItems2 as IList;
if (selectedItems2List == null) { return; }
try
{
_isMergingItems = true;
foreach (var item in selectedItems2List)
{
SelectedItems.Add(item);
}
}
finally
{
_isMergingItems = false;
}
}
}
USAGE: XAML
<localUI:MyListView ItemsSource="{Binding Roster}"
SelectedItems2="{Binding SelectedPlayers}"
SelectionMode2="{Binding IsEditingGameRoster, Converter={StaticResource ListViewSelectionModeFromBooleanNoneOrMultipleConverter}}">
<ListView.ItemTemplate>
<DataTemplate>
<!-- List View Display Not important for describing problem. -->
</DataTemplate>
</ListView.ItemTemplate>
</localUI:MyListView>
USAGE: ViewModel
private ObservableCollection<Player> _selectedPlayers;
public ObservableCollection<Player> SelectedPlayers
{
get
{
if (_selectedPlayers == null)
{
_selectedPlayers = new ObservableCollection<Player>();
}
return _selectedPlayers;
}
}
Related
(Question modified for better clarity)
I've been researching all day and nothing really clears up my issue. I have a combobox that gets its source from a collection of a "Bonus" that has a Name and a Code.
<ComboBox Margin="4"
SelectedItem="{Binding Path=SelectedBonus, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding Path=Bonuses, UpdateSourceTrigger=Explicit}"
DisplayMemberPath="Name"
IsSynchronizedWithCurrentItem="True">
"Bonuses" is a collection that is retrieved from a file initially. The combobox is properly filled with the collection, and I can select any of the items in the list.
Can't post image, so here is the link to the loaded list on startup: https://www.flickr.com/photos/mav_2007/14059399414/in/set-72157644402662271
Now, the following code is called on a context switch (right click the combo-box and select "edit Bonus"). However, here is where I cannot make any sense of what's going on with WPF and bindings.
In the code below, the method called "EditBonus" resets the combobox list be re-reading the file to make sure nothing has changed (Bonuses.Clear() is called, then rebuilt, and SelectedItem set). But, as soon as I make the call to ShowDialog, the combobox selected item goes blank. If I un-comment the OnPropertyChanged calls below, the problem appears fixed. But, I do not understand why that works, and there is a case I can't fix where an exception in the Save operation of the dialog makes that combo box disappear again.
/// <summary>
/// Handle the "edit bonus" command
/// </summary>
void OnEditBonus()
{
// only edit this is we have a bonus selected
if (bonusEditViewModel.EditBonus())
{
// OnPropertyChanged("SelectedBonus");
var window = new BonusEditDialog(bonusEditViewModel);
window.ShowDialog();
// OnPropertyChanged("SelectedBonus");
}
}
And the method EditBonus():
internal bool EditBonus()
{
bool success = false;
if (SelectedBonus != null)
{
var originalCode = SelectedBonus.Code;
success = UpdateBonuses(originalCode);
if (success)
{
if (SelectedBonus.Code == originalCode)
{
BonusName = SelectedBonus.Type;
originalBonusName = BonusName;
CloseWindow = false;
}
}
}
return success;
}
Now, I've tried changing the UpdateSourceTrigger types and the modes and nothing makes a difference.
I believe it is related to the changing of the datacontext when accessing the dialog, and yet still making modification to the combobox's itemSource. But I'm not sure how I can defer the combobox update until the dialog exits.
This is what it looks like if I try to edit the list without calling that OnPropertyChanged after returning from EditBonus() (note the empty combobox):
https://www.flickr.com/photos/mav_2007/14035815226/in/set-72157644402662271
And this is what it looks like if I make the call just after returning from EditBonus():
And I would love to show you this, but I guess I'm not "reputable" enough :( (You can see all images for this issue on my Flickr page from that link)
Thanks for any help you can give
Additional information (Possible culprit identified):
The DataContext for SelectedBonus is BehaviorViewModel, but there is another SelectedBonusProperty inside BonusEditViewModel. Both BehaviorViewModel and BonusEditViewModel derive from ViewModelBase, which is where the OnPropertyChanged handler actually executes the handler based on ViewModel type. BehaviorViewModel is what is bound to the form. Calling OnPropertyChanged inside BonusEditViewModel has no effect because it is a different context.
Here is the SelectedBonus property inside BehaviorViewMode:
/// <summary>
/// Expose the bonus information from the bonusEditViewModel
/// </summary>
public Bonus SelectedBonus
{
get
{
return bonusEditViewModel.SelectedBonus;
}
set
{
bonusEditViewModel.SelectedBonus = value;
if (behavior != null && bonusEditViewModel.SelectedBonus != null)
{
behavior.Items[BehaviorItem.THEME_ID].Value = bonusEditViewModel.SelectedBonus.Code.ToString();
}
if (bonusEditViewModel.SelectedBonus != null)
{
ThemeShortname = bonusEditViewModel.SelectedBonus.Type;
}
OnPropertyChanged("SelectedBonus");
}
}
And here is the code for the SelectedBonus inside BonusEditViewModel:
/// <summary>
/// The currently selected bonus which will be edited by the dialog
/// </summary>
public Bonus SelectedBonus
{
get { return selectedBonus; }
set
{
selectedBonus = value;
OnPropertyChanged("SelectedBonus");
}
}
The function OnEditBonus() exists inside the BehaviorViewModel, and it calls the method UpdateBonuses() on the BonusEditViewModel object. Now I'm wondering the best way to fix this...
The problem was verified to be with the structure of the code, which had a binding to the property SelectedBonus in the ViewModel class called BehaviorViewModel. However, the property in this class actually gets its data from a property in the ViewModel class called BonusEditViewModel. Calling "OnPropertyChanged("SelectedBonus") inside the BonusEditViewModel class has no effect on the property bound in BehaviorViewModel. The code needs to be restructured so that the SelectedBonus property in BehaviorViewModel does not reference the property in another ViewModel. This is just poorly structured code.
Edit: I have already looked at the Infragistics forums. The code below is based on their samples. The data binding just doesn't seem to work.
I have an Infragistics XamDataGrid in my view.
<DockPanel>
<grid:XamDataGrid x:Name="gridData" DataContext="{Binding Path=DataEditorDataTable}" DataSource="{Binding Path=DataEditorDataTable.DefaultView}"
IsSynchronizedWithCurrentItem="True" Visibility="{Binding DataGridVisible}">
<grid:XamDataGrid.FieldLayoutSettings>
<grid:FieldLayoutSettings AutoGenerateFields="True" AllowAddNew="False" AllowDelete="False" />
</grid:XamDataGrid.FieldLayoutSettings>
</grid:XamDataGrid>
</DockPanel>
I set the data context for the user control in the constructor.
public DataEditor(SomeDataType DataType, IEventAggregator eventaggregator)
{
InitializeComponent();
this.DataContext = new DataEditorViewModel(DataType, eventaggregator);
}
In the dataeditor view model, I subscribe to an event that lets me know when data has changed and I build a datatable and call a method SetData. (I cannot know ahead how many columns of data are going to be shown in the grid, and these columns keep changing with user interaction, so I am hoping to use the data table to bind.)
I assign the properties in a method like so.
/// <summary>
/// Returns the data that the data editor displays.
/// </summary>
public DataTable DataEditorDataTable
{
get
{
return dtDataEditor;
}
set
{
dtDataEditor = value;
OnPropertyChanged("DataEditorDataTable");
}
}
/// <summary>
/// Method to set data on load
/// </summary>
private void SetData(DataTable dtDataEditor)
{
if (!isDataEditorCellEdited)
if (dtDataEditor != null && dtDataEditor.Rows.Count > 0)
{
try
{
//Assign the data to the grid
DataEditorDataTable = dtDataEditor;
DataGridVisible = Visibility.Visible;
}
catch
{
//If any exception occurs, hide the grid
DataGridVisible = Visibility.Collapsed;
}
}
else
//If no data, hide the grid
DataGridVisible = Visibility.Collapsed;
}
The problem is that the binding is simply not happening. Is there anything in particular I have missed with regard to the bindings?
For debugging the binding errors you should look at the output window in visual studio to see if there are any errors.
Reading the code that you have, I assume that the binding is incorrect and should be:
DataContext="{Binding Path=DataEditorDataTable}" DataSource="{Binding Path=DefaultView}"
The change that I made was to remove the property from the table from the path of the DataSource since the table is already the DataContext and you want to bind to the DefaultView off of the table.
I would like to know every time a user modifies data in WPF DataGrid.
Is there a single event that I can use to do that? Or what is the minimal set of events that I can use to cover full set of data changes (Add row, delete row, modify row etc)?
I know that this is probably more than you are asking for, but once you do it, it's hard to go back. Whatever you are binding to ... some List, have that item implement IEditableObject.
that way you won't have to ever worry about whatever control/view implementation, events ets.
When the item is changed, the datagrid as well as plethora of .NET controls will set the IsDirty object to true.
These are not super great links but they will get you started thinking about maintaining isDirty flag.
https://msdn.microsoft.com/en-us/library/system.componentmodel.ieditableobject(v=vs.110).aspx
object editing and isDirty() flag
http://bltoolkit.net/doc/EditableObjects/EditableObject.htm
this is more what I am used to:
https://stackoverflow.com/a/805695/452941
Usually, when you are using MVVM, you bind the master list to an ObservableCollection and then the selected item to a specific instance. Inside your setters, you can raise events. This would be the most logical (read: the most common method I've seen) to capture updates / adds / deletes to a list of data.
MainWindow.xaml
<Window x:Class="WpfApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication"
Title="MainWindow" Height="350" Width="525">
<DataGrid x:Name="dataGrid" AutoGeneratingColumn="OnAutoGeneratingColumn">
<DataGrid.Resources>
<Style TargetType="DataGridCell">
<EventSetter Event="Binding.SourceUpdated" Handler="OnDataGridCellSourceUpdated"/>
<EventSetter Event="Binding.TargetUpdated" Handler="OnDataGridCellTargetUpdated"/>
</Style>
</DataGrid.Resources>
</DataGrid>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.dataGrid.ItemsSource = new ObservableCollection<Person>()
{
new Person() { Name = "John", Surname = "Doe" },
new Person() { Name = "Jane", Surname = "Doe" }
};
}
private void OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
var dataGridBoundColumn = e.Column as DataGridBoundColumn;
if (dataGridBoundColumn != null)
{
var binding = dataGridBoundColumn.Binding as Binding;
if (binding != null)
{
binding.NotifyOnSourceUpdated = true;
binding.NotifyOnTargetUpdated = true;
}
}
}
private void OnDataGridCellSourceUpdated(object sender, DataTransferEventArgs e)
{
this.OnDataGridCellChanged((DataGridCell)sender);
}
private void OnDataGridCellTargetUpdated(object sender, DataTransferEventArgs e)
{
this.OnDataGridCellChanged((DataGridCell)sender);
}
private void OnDataGridCellChanged(DataGridCell dataGridCell)
{
// DataContext is MS.Internal.NamedObject for NewItemPlaceholder row.
var person = dataGridCell.DataContext as Person;
if (person != null)
{
var propertyName = ((Binding)((DataGridBoundColumn)dataGridCell.Column).Binding).Path.Path;
var propertyValue = TypeDescriptor.GetProperties(person)[propertyName].GetValue(person);
// TODO: do some logic here.
}
}
}
This is what I used for some complex DataGridCell formatting based on a Person (just some POCO) instance, property name and property value.
But if you want to be able to know when to save the data and you use MVVM, then the best way to do this would be to have original value and current value for every editable property in your view model / model. When data is loaded, original and current value would be equal, and if property is changed through DataGrid or any other way, only current value is updated. When data needs to be saved, just check if any item has any property that has different original and current value. If the answer is yes, then data should be saved, because it has been changed since last load / save, otherwise data is same as when loaded, so no new saving is required. Also, when saving, current values must be copied to original values, because data is again equal to the saved data, like when it was last loaded.
if you use mvvm you do not need to know when the "user modify data in the data grid" you have to know when the underlying collection change.
so if you use datatable(HasChanges/RejectChanges...) you have that all built in. if you use poco collections then your items at least have to implement INotifyPropertyChanged - if its raised the user modify data. Maybe IEditable is a good one too for reject changes and so on.
I am trying to migrate a small prototype application I made in WinForms to WPF. I'm having some issues with a combobox in WPF not changing values when I select a different value from the drop-down. Initially, I tried just copying the code that I used in my WinForms app to populate the combobox and determine if a new index had been selected. This is how my WinForms code looked like:
private void cmbDeviceList_SelectedIndexChanged(object sender, EventArgs e)
{
var cmb = (Combobox) sender;
var selectedDevice = cmb.SelectedItem;
var count = cmbDeviceList.Items.Count;
// find all available capture devices and add to drop down
for(var i =0; i<count; i++)
{
if(_deviceList[i].FriendlyName == selectedDevice.ToString())
{
_captureCtrl.VideoDevices[i].Selected = true;
break;
}
}
}
Earlier in the code, I am populating the _deviceList List and the combo box (in Form1_Load to be specific) by looping over the available devices and adding them. I tried the same approach in WPF and could only populate the combo box. When I selected a new value, for some reason the same exact value (the initial device) was being sent into the event code (cmbCaptureDevices_SelectionChanged in my WPF app). I looked around for some tutorials in WPF and found that maybe data binding was my issue, and I tried that out instead. This is my combobox in my XAML file:
<ComboBox ItemsSource="{Binding Devices}" Name="cmbCaptureDevices"
IsSynchronizedWithCurrentItem="True" SelectedItem="{Binding CurrentDevice,
Mode=TwoWay}" Se;ectionChanged="cmbCapturedDevices_SelectionChanged" />
There's more to that XAML definition, but it's all arbitrary stuff like HorizontalAlignment and whatnot. My VideoDevicesViewModel inherits from INotifyPropertyChanged, has a private List<Device> _devices and a private Device _currentDevice. The constructor looks like:
public VideoDevicesViewModel()
{
_devices = GetCaptureDevices();
DevicesCollection = new CollectionView(_devices);
}
GetCaptureDevices simply is the loop that I had in my WinForms app which populates the list with all avaialble capture devices on the current machine. I have a public CollectionView DevicesCollection { get; private set; } for getting/setting the devices at the start of the application. The property for my current device looks like:
public Device CurrentDevice
{
get { return _currentDevice; }
set
{
if (_currentDevice = value)
{
return;
}
_currentDevice = value;
OnPropertyChanged("CurrentDevice");
}
}
OnPropertyChanged just raises the event PropertyChanged if the event isn't null. I'm new to WPF (and pretty new to C# in general, honestly) so I'm not sure if I'm missing something elementary or not. Any idea as to why this combobox won't change values for me?
Discovered the answer on my own here. The unexpected behavior was a result of using the Leadtools Device class. It's a COM component and apparently was not playing nicely with my application. I honestly don't understand why exactly it worked, but I wrapped the Device class in another class and used that instead. As soon as I was using the wrapper class, the combo box functioned as it should.
You are using the assignment operator '=' instead of the equality operator '=='
Change
if (_currentDevice = value)
to
if (_currentDevice == value)
Try the following
if _currentDevice == value ...
I am binding a WPF Datagrid's ItemsSource property to an ObservableCollection. The data gets pulled in fine but I notice strange behavior when I click on a cell (any cell) and start using the keyboard to navigate.
Tab key works as expected (left-to-right and wraps to the next line down).
Up key does nothing (focus stays on the selected cell).
Down key shifts the focus to the top cell in the column. For example, if I'm on row 10 of column B and hit "down" then the focused cell becomes row 0 of column B.
Left or right keys cause an ArgumentOutOfRangeException. "Specified argument was out of the range of valid values. Parameter name: index". There is no InnerException and the Stack Trace isn't much help either.
Double-clicking any of the cells doesn't enable edit mode.
For some reason I get an "extra" row (rowcount=(collection count +1)) at the bottom of the grid that has correct functionality (right/left buttons work, double-click triggers edit mode). When I double click to enter edit mode in this extra row and then click in a bound row above it, an extra row is added to the grid.
I believe I've isolated the incident to the bound ViewModel class (as opposed to the XAML). In preliminary debugging I've stripped the XAML down to the bare minimum (no styles, 2 columns, only binding is the ItemsSource) and still get the weird behavior.
<UserControl x:Name="FuelMileageView" x:Class="TRD.RaceStrategy.Views.FuelMileage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:TRD.RaceStrategy"
xmlns:views="clr-namespace:TRD.RaceStrategy.Views"
xmlns:vm="clr-namespace:TRD.RaceStrategy.ViewModels;assembly=RaceStrategy.Support"
xmlns:converters="clr-namespace:TRD.RaceStrategy.Converters;assembly=RaceStrategy.Support"
xmlns:Behaviours="clr-namespace:TRD.RaceStrategy.Behaviours;assembly=RaceStrategy.Support"
mc:Ignorable="d" DataContext="{Binding Path=Properties[PrimaryVM].CarEventVM.FuelMileage.FuelMileageLaps, Source={x:Static local:App.Current}}"
d:DesignHeight="410" d:DesignWidth="485">
<DataGrid x:Name="dgFMLaps" ItemsSource="{Binding}"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Column 1" >
</DataGridTextColumn>
<DataGridTextColumn Header="Column 2" >
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</UserControl>
Aside from the InitializeComponent() call there is no codebehind to speak of which seems to leave the bound FuelMileageLaps collection as the only culprit.
public class FuelMileageLapViewModel : LapViewModel
{
public FuelMileageLapViewModel() { }
}
NOTE: ObservableCollectionEx is an extension of the ObservableCollection class which apparently accounts for threading problems (?) I've used this class with other collections that I've in turn plugged into datagrids that didn't have this keyboard navigation problem.
public class ObservableCollectionEx<T> : ObservableCollection<T>
{
// Override the event so this class can access it
public override event System.Collections.Specialized.NotifyCollectionChangedEventHandler CollectionChanged;
protected override void OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
// Be nice - use BlockReentrancy like MSDN said
using (BlockReentrancy())
{
System.Collections.Specialized.NotifyCollectionChangedEventHandler eventHandler = CollectionChanged;
if (eventHandler == null)
return;
Delegate[] delegates = eventHandler.GetInvocationList();
// Walk thru invocation list
foreach (System.Collections.Specialized.NotifyCollectionChangedEventHandler handler in delegates)
{
DispatcherObject dispatcherObject = handler.Target as DispatcherObject;
// If the subscriber is a DispatcherObject and different thread
if (dispatcherObject != null && dispatcherObject.CheckAccess() == false)
{
// Invoke handler in the target dispatcher's thread
dispatcherObject.Dispatcher.Invoke(DispatcherPriority.DataBind, handler, this, e);
}
else // Execute handler as is
handler(this, e);
}
}
}
}
At this point I'm at my wit's end as to the next step for debugging. There's no obvious place to throw in a breakpoint or try/catch block. I've Googled my problem to death and haven't found anything worthwhile. Please help!
Here's the FuelMileageViewModel class where FuelMileageLaps is initialized:
public class FuelMileageViewModel : WorkspaceViewModel
{
/// <summary>
/// View model for the fuel mileage calculator
/// </summary>
/// <param name="car">Car on which to base all calculations</param>
public FuelMileageViewModel()
{}
/// <summary>
/// A separate collection of laps that store the extra fuel mileage data
/// </summary>
public ObservableCollectionEx<FuelMileageLapViewModel> FuelMileageLaps
{
get
{
if (_fuelMileageLaps == null)
{
_fuelMileageLaps = new ObservableCollectionEx<FuelMileageLapViewModel>();
}
return _fuelMileageLaps;
}
set
{
_fuelMileageLaps = value;
OnPropertyChanged("FuelMileageLaps");
}
}
private ObservableCollectionEx<FuelMileageLapViewModel> _fuelMileageLaps;
/// <summary>
/// Number of laps in the race
/// Affects: Laps_G, Laps_Y
/// </summary>
public int NumberOfLaps
{
get
{
return FuelMileageLaps.Count;
}
set
{
int count = FuelMileageLaps.Count;
if (value < 0)
{
throw new ArgumentException("Number of laps must be a positive integer");
}
if (count != value)
{
if( count < value )
{
int diff = value - count;
for (int i = 0; i < diff; i++)
{
FuelMileageLapViewModel lapToAdd = new FuelMileageLapViewModel();
FuelMileageLaps.Add(lapToAdd);
}
}
OnPropertyChanged("NumberOfLaps");
}
}
}
}
I know this is an older question, but I was having VERY similar problems with a DataGrid recently. The arrow key problems were just as you described. However, I used DataGridTemplateColumns in which a TextBox allowed for editing (otherwise, as you noted, I couldn't edit the data). If I edited a cell and then clicked out of it (particularly if I clicked the row above it) the DataGrid would often produce a duplicate row (though the count in the collection remained the same).
In my case, I found a bug in my Equals(object obj) method for the class whose members were in the ObservableCollection being bound to the DataGrid. This prevented calls to Equals from determining the true equality between two items, and somehow this caused big issues. For example, if after editing a row the data grid can't find that same row already in its list (via calls to a buggy Equals method), perhaps it generates another row (assuming it must not have a row yet for that entry).
In any case, I'd suggest checking out the Equals method in your LapViewModel.
Hope that helps.