In wpf I setup a tab control that binds to a collection of objects each object has a data template with a data grid presenting the data. If I select a particular cell and put it into edit mode, leaving the grid by going to another tab this will cause the exception below to be thrown on returning the datagrid:
'DeferRefresh' is not allowed during an AddNew or EditItem transaction.
It appears that the cell never left edit mode. Is there an easy way to take the cell out of edit mode, or is something else going on here?
Update: It looks like if I do not bind the tab control to the data source, but instead explicitly define each tab and then bind each item in the data source to a content control this problem goes away. This is not really a great solution, so I would still like to know how to bind the collection directly to the tab control.
Update: So what I have actually done for my own solution is to use a ListView and a content control in place of a tab control. I use a style to make the list view look tab like. The view model exposes a set of child view models and allows the user to select one via the list view. The content control then presents the selected view model and each view model has an associated data template which contains the data grid. With this setup switching between view models while in edit mode on the grid will properly end edit mode and save the data.
Here is the xaml for setting this up:
<ListView ItemTemplate="{StaticResource MakeItemsLookLikeTabs}"
ItemsSource="{Binding ViewModels}"
SelectedItem="{Binding Selected}"
Style="{StaticResource MakeItLookLikeATabControl}"/>
<ContentControl Content="{Binding Selected}">
I'll accept Phil's answer as that should work also, but for me the solution above seems like it will be more portable between projects.
I implemented a behavior for the DataGrid based on code I found in this thread.
Usage:<DataGrid local:DataGridCommitEditBehavior.CommitOnLostFocus="True" />
Code:
using System.Collections.Generic;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
/// <summary>
/// Provides an ugly hack to prevent a bug in the data grid.
/// https://connect.microsoft.com/VisualStudio/feedback/details/532494/wpf-datagrid-and-tabcontrol-deferrefresh-exception
/// </summary>
public class DataGridCommitEditBehavior
{
public static readonly DependencyProperty CommitOnLostFocusProperty =
DependencyProperty.RegisterAttached(
"CommitOnLostFocus",
typeof(bool),
typeof(DataGridCommitEditBehavior),
new UIPropertyMetadata(false, OnCommitOnLostFocusChanged));
/// <summary>
/// A hack to find the data grid in the event handler of the tab control.
/// </summary>
private static readonly Dictionary<TabPanel, DataGrid> ControlMap = new Dictionary<TabPanel, DataGrid>();
public static bool GetCommitOnLostFocus(DataGrid datagrid)
{
return (bool)datagrid.GetValue(CommitOnLostFocusProperty);
}
public static void SetCommitOnLostFocus(DataGrid datagrid, bool value)
{
datagrid.SetValue(CommitOnLostFocusProperty, value);
}
private static void CommitEdit(DataGrid dataGrid)
{
dataGrid.CommitEdit(DataGridEditingUnit.Cell, true);
dataGrid.CommitEdit(DataGridEditingUnit.Row, true);
}
private static DataGrid GetParentDatagrid(UIElement element)
{
UIElement childElement; // element from which to start the tree navigation, looking for a Datagrid parent
if (element is ComboBoxItem)
{
// Since ComboBoxItem.Parent is null, we must pass through ItemsPresenter in order to get the parent ComboBox
var parentItemsPresenter = VisualTreeFinder.FindParentControl<ItemsPresenter>(element as ComboBoxItem);
var combobox = parentItemsPresenter.TemplatedParent as ComboBox;
childElement = combobox;
}
else
{
childElement = element;
}
var parentDatagrid = VisualTreeFinder.FindParentControl<DataGrid>(childElement);
return parentDatagrid;
}
private static TabPanel GetTabPanel(TabControl tabControl)
{
return
(TabPanel)
tabControl.GetType().InvokeMember(
"ItemsHost",
BindingFlags.NonPublic | BindingFlags.GetProperty | BindingFlags.Instance,
null,
tabControl,
null);
}
private static void OnCommitOnLostFocusChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
var dataGrid = depObj as DataGrid;
if (dataGrid == null)
{
return;
}
if (e.NewValue is bool == false)
{
return;
}
var parentTabControl = VisualTreeFinder.FindParentControl<TabControl>(dataGrid);
var tabPanel = GetTabPanel(parentTabControl);
if (tabPanel != null)
{
ControlMap[tabPanel] = dataGrid;
}
if ((bool)e.NewValue)
{
// Attach event handlers
if (parentTabControl != null)
{
tabPanel.PreviewMouseLeftButtonDown += OnParentTabControlPreviewMouseLeftButtonDown;
}
dataGrid.LostKeyboardFocus += OnDataGridLostFocus;
dataGrid.DataContextChanged += OnDataGridDataContextChanged;
dataGrid.IsVisibleChanged += OnDataGridIsVisibleChanged;
}
else
{
// Detach event handlers
if (parentTabControl != null)
{
tabPanel.PreviewMouseLeftButtonDown -= OnParentTabControlPreviewMouseLeftButtonDown;
}
dataGrid.LostKeyboardFocus -= OnDataGridLostFocus;
dataGrid.DataContextChanged -= OnDataGridDataContextChanged;
dataGrid.IsVisibleChanged -= OnDataGridIsVisibleChanged;
}
}
private static void OnDataGridDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var dataGrid = (DataGrid)sender;
CommitEdit(dataGrid);
}
private static void OnDataGridIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var senderDatagrid = (DataGrid)sender;
if ((bool)e.NewValue == false)
{
CommitEdit(senderDatagrid);
}
}
private static void OnDataGridLostFocus(object sender, KeyboardFocusChangedEventArgs e)
{
var dataGrid = (DataGrid)sender;
var focusedElement = Keyboard.FocusedElement as UIElement;
if (focusedElement == null)
{
return;
}
var focusedDatagrid = GetParentDatagrid(focusedElement);
// Let's see if the new focused element is inside a datagrid
if (focusedDatagrid == dataGrid)
{
// If the new focused element is inside the same datagrid, then we don't need to do anything;
// this happens, for instance, when we enter in edit-mode: the DataGrid element loses keyboard-focus,
// which passes to the selected DataGridCell child
return;
}
CommitEdit(dataGrid);
}
private static void OnParentTabControlPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var dataGrid = ControlMap[(TabPanel)sender];
CommitEdit(dataGrid);
}
}
public static class VisualTreeFinder
{
/// <summary>
/// Find a specific parent object type in the visual tree
/// </summary>
public static T FindParentControl<T>(DependencyObject outerDepObj) where T : DependencyObject
{
var dObj = VisualTreeHelper.GetParent(outerDepObj);
if (dObj == null)
{
return null;
}
if (dObj is T)
{
return dObj as T;
}
while ((dObj = VisualTreeHelper.GetParent(dObj)) != null)
{
if (dObj is T)
{
return dObj as T;
}
}
return null;
}
}
I have managed to work around this issue by detecting when the user clicks on a TabItem
and then committing edits on visible DataGrid
in the TabControl
. I'm assuming the user will expect their changes to still be there when they click back.
Code snippet:
// PreviewMouseDown event handler on the TabControl
private void TabControl_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (IsUnderTabHeader(e.OriginalSource as DependencyObject))
CommitTables(yourTabControl);
}
private bool IsUnderTabHeader(DependencyObject control)
{
if (control is TabItem)
return true;
DependencyObject parent = VisualTreeHelper.GetParent(control);
if (parent == null)
return false;
return IsUnderTabHeader(parent);
}
private void CommitTables(DependencyObject control)
{
if (control is DataGrid)
{
DataGrid grid = control as DataGrid;
grid.CommitEdit(DataGridEditingUnit.Row, true);
return;
}
int childrenCount = VisualTreeHelper.GetChildrenCount(control);
for (int childIndex = 0; childIndex < childrenCount; childIndex++)
CommitTables(VisualTreeHelper.GetChild(control, childIndex));
}
This is in the code behind.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With