Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the proper way to handle multiple datagrids in a tab control so that cells leave edit mode when the tabs are changed?

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.

like image 821
Jon Avatar asked Jul 28 '10 16:07

Jon


2 Answers

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;
    }
}
like image 51
Robert Hahn Avatar answered Nov 17 '22 23:11

Robert Hahn


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.

like image 28
Phil Gan Avatar answered Nov 18 '22 00:11

Phil Gan