JFTxJ
JFTxJ

Reputation: 552

WPF DataGrid - Retain selection when disabling

I have been struggling with this for a while now. I have a Master / Details layout in my application, and am faced, like many others, with the problem of the DataGrid loosing its selection when disabling it. Essencialy, after selecting an element from the list to populate a series of fields, the user presses "Edit", wich disables the DataGrid and enables all of the form's fields. Pressing the "Save" button will revert these actions after saving the data... Pretty strait forward.

I am on Windows 7 developping with VS 2010 in the .Net Framework 4.

What I have tried:
1) Based on this post, I have tried to use the DataGrid in the June 2009 version of the WPF Toolkit, but I had the same reaction.
2) Based on this WPF CodePlex bug report, I have tried to create a custom control based on the DataGrid and to override the OnIsEnabledChanged call to remove the call to "UnselectAllCells", but with no code example, I can't even get it to fire once. I have tried:

public class FormMainDataGrid : DataGrid
{
    static FormMainDataGrid()
    {
        IsEnabledProperty.OverrideMetadata(typeof(FormMainDataGrid), new FrameworkPropertyMetadata(new PropertyChangedCallback(OnIsEnabledChanged)));
    }

    public FormMainDataGrid() : base() { }

    private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        d.CoerceValue(CanUserAddRowsProperty);
        d.CoerceValue(CanUserDeleteRowsProperty);

        //this was added in new version !!!
        /*
        if (!(bool)(e.NewValue))
        {
            ((DataGrid)d).UnselectAllCells();
        }
        */

        // Many commands use IsEnabled to determine if they are enabled or not
        CommandManager.InvalidateRequerySuggested();
    }
}  

but this still unselects the currently selected row as soon as I disable the DataGrid. I have tried to interprete the last comments (in the Codeplex bug report) like this:

public class FormMainDataGrid : DataGrid
{
    static FormMainDataGrid()
    {

    }

    public static void OverrideStuff() 
    {
        IsEnabledProperty.OverrideMetadata(typeof(FormMainDataGrid), new FrameworkPropertyMetadata(new PropertyChangedCallback(OnIsEnabledChanged)));
    }

    public FormMainDataGrid() : base() { }

    private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        d.CoerceValue(CanUserAddRowsProperty);
        d.CoerceValue(CanUserDeleteRowsProperty);

        //this was added in new version !!!
        /*
        if (!(bool)(e.NewValue))
        {
            ((DataGrid)d).UnselectAllCells();
        }
        */

        // Many commands use IsEnabled to determine if they are enabled or not
        CommandManager.InvalidateRequerySuggested();
    }
}

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        FormMainDataGrid.OverrideStuff();
        base.OnStartup(e);
    }
}  

but that does not even fire the modified version of the method.

First, am-I going the right way for this? Considering that the Deselection is caused by this method, can I completely replace the internal call to 'OnIsEnabledChanged' for my own method? Is there another way I could be tackling this problem? Or more specificly, how can i stop the call to the base version of this method since it is not an override, thus I cannot 'not' call the base.OnIsEnabledChanged?

Thanks alot!

Upvotes: 2

Views: 1603

Answers (3)

KoenJ
KoenJ

Reputation: 1141

For future reference, if anyone is running into the same issue.
Re-setting the SelectedValue has a lot of side-effects.
This is the correct way to override the metadata on the Grid:

public class MyDataGrid : DataGrid
{
    static MyDataGrid()
    {
        IsEnabledProperty.OverrideMetadata(typeof(MyDataGrid), new CustomFrameworkPropertyMetadata(OnIsEnabledChanged));
    }

    /// <summary>
    /// Fixes the issue that the DataGrid's selection is cleared whenever the DataGrid is disabled.
    /// Tricky: this issue only happens for 4.0 installations, it is fixed in 4.5 (in-place upgrade) installations.
    /// </summary>
    /// <param name="d"></param>
    /// <param name="e"></param>
    private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        d.CoerceValue(CanUserAddRowsProperty);
        d.CoerceValue(CanUserDeleteRowsProperty);

        //this is there in 4.0 dlls, not in the in-place upgrade 4.5 dlls.
        //if (!(bool)(e.NewValue))
        //{
        //    ((DataGrid)d).UnselectAllCells();
        //}

        CommandManager.InvalidateRequerySuggested();
    }

    class CustomFrameworkPropertyMetadata : FrameworkPropertyMetadata
    {
        public CustomFrameworkPropertyMetadata(PropertyChangedCallback propertyChangedCallback)
            : base(propertyChangedCallback)
        {
        }

        protected override void Merge(PropertyMetadata baseMetadata, DependencyProperty dp)
        {
            // See: http://msdn.microsoft.com/en-us/library/system.windows.propertymetadata.merge.aspx
            // See: http://msdn.microsoft.com/en-us/library/ms751554.aspx
            // By default, PropertyChangedCallbacks are merged from all owners in the inheritance hierarchy,
            // so all callbacks are called whenever the property changes.
            var thisPropertyChangedCallback = this.PropertyChangedCallback;

            base.Merge(baseMetadata, dp);

            // We do NOT want that default behavior here;
            // The callback of DataGrid should not be called here - it clears the selection, we don't want that.
            // But the callback of UIElement should be called here - it visually disabled the element, we still want that.
            if (baseMetadata.PropertyChangedCallback != null)
            {
                Delegate[] invocationList = baseMetadata.PropertyChangedCallback.GetInvocationList();
                PropertyChangedCallback inheritedPropertyChangedCallback = null;
                foreach (var invocation in invocationList)
                {
                    if (invocation.Method.DeclaringType == typeof(DataGrid))
                    {
                        // Do nothing; don't want the callback from DataGrid that clears the selection.
                    }
                    else
                    {
                        inheritedPropertyChangedCallback = inheritedPropertyChangedCallback == null
                            ? (PropertyChangedCallback)invocation
                            : (PropertyChangedCallback)Delegate.Combine(inheritedPropertyChangedCallback, invocation);
                    }

                }
                this.PropertyChangedCallback = thisPropertyChangedCallback != null
                                                   ? (PropertyChangedCallback)Delegate.Combine(inheritedPropertyChangedCallback, thisPropertyChangedCallback)
                                                   : inheritedPropertyChangedCallback;
            }
        }
    }
}



Note that the issue mentioned in this post only happens in 4.0 installations without 4.5 installed.
It is 'fixed' in .net 4.5, even for applications targeting 4.0 (the "4.5 is an in-place upgrade" scenario / misery).

Regards,
Koen

Upvotes: 3

JFTxJ
JFTxJ

Reputation: 552

The same problem with the Up-Down key still exists with IsHitTestVisible = false.

So what I ended up doing is re-working the custom control like this:

    public class FormMainDataGrid : DataGrid
    {
        public FormMainDataGrid() : base() {
            this.IsEnabledChanged += new DependencyPropertyChangedEventHandler(DataGrid_IsEnabledChanged);
            this.SelectionChanged += new SelectionChangedEventHandler(DataGrid_SelectionChanged);
        }

        private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs args)
        {
            if (this.IsEnabled)
            {
                _selectedValue = this.SelectedValue;
            }
        }

        private object _selectedValue;

        private void DataGrid_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs args)
        {
            this.Dispatcher.BeginInvoke((Action)(() =>
            {
                this.SelectedValue = _selectedValue;
            }), null);
        }
    }

This works pretty well... I just have to be carefull because changing the SelectedValue when the control is disable will then put it off track...

So in conclusion, I believe that your solution is the most complete, but mine allows me to keep my form's code as lean & mean as possible.

Thanks for your help!

Upvotes: 0

CodeWarrior
CodeWarrior

Reputation: 7470

I generally don't disable controls specifically for this reason. I have found it much better to either collapse the control which keeps its databinding current, or if I must keep it visible but disallow any kind of interaction, put a partially transparent black border over it that is normally collapsed and becomes visible on command.

Upvotes: 1

Related Questions