1
votes

I'm trying to come up with a System.Windows.Interactivity.Behaviour that, when applied to a WPF DataGrid, adds a context menu (or items to an existing context menu) that allow users to show or hide columns.

I came up with a solution that almost works very well. Everything works just as expected - until you hide and then re-show a column. Once becoming visible again, the contextmenu just seems to disappear, right-clicking on the column doesn't do anything anymore.

Code is below, verbalyl what I'm doing:

  1. On attaching the behavior, I start listening to the DataGrid "Loaded" event
  2. In the Loaded event, I find all DataGridColumnHeader descendants of the DataGrid
  3. For each of these, I generate an individual context menu and attach it to the DataGridColumnHeader
  4. For each context menu, I generate one menu item per column, and assign a command to it that, upon execution, sets the DataGridColumn's visibility to Visible or Hidden

I've stripped down the code to a minimum example for the simplest case: To test this, simply apply that behavior to a DataGrid that doesn't have a ContextMenu assigned currently.

public class DgColumnBehavior : Behavior<DataGrid>
{
    protected ICommand ToggleColumnVisibilityCmd;
    protected DataGrid _AssociatedObject;

    protected override void OnAttached()
    {
        this.ToggleColumnVisibilityCmd = new DelegateCommand<DataGridColumn>(ToggleColumnVisibilityCmdExecute);
        this._AssociatedObject = (DataGrid)this.AssociatedObject;

        Observable.FromEventPattern(this._AssociatedObject, "Loaded")
            .Take(1)
            .Subscribe(x => _AssociatedObject_Loaded());

        base.OnAttached();
    }

    void _AssociatedObject_Loaded()
    {
        var columnHeaders = this._AssociatedObject.SafeFindDescendants<DataGridColumnHeader>();  // see second code piece for the SafeFindDescendants extension method

        foreach (var columnHeader in columnHeaders)
        {
            EnsureSeparateContextMenuFor(columnHeader);

            if (columnHeader.ContextMenu.ItemsSource != null)
            {
                // ContextMenu has an ItemsSource, so need to add items to that -
                // ommitted though as irrelevant for example
            }
            else
            {
                // No ItemsSource assigned to the Menu, so we can just add directly

                foreach (var item in CreateMenuItemsFor(columnHeader))
                    columnHeader.ContextMenu.Items.Add(item);
            }
        }
    }

    /// Ensures that the columnHeader ...
    /// A) has a ContextMenu, and
    /// B) that is has an individual context menu, i.e. one that isn't shared with any other DataGridColumnHeaders.
    /// 
    /// I'm doing that as in practice, I'm adding some further items that are specific to each column, so I can't have a shared context menu
    private void EnsureSeparateContextMenuFor(DataGridColumnHeader columnHeader)
    {
        if (columnHeader.ContextMenu == null)
        {
            columnHeader.ContextMenu = new ContextMenu();
        }
        else
        {
            // clone the existing menu
            // ommitted as irrelevant for example
        }
    }

    /// Creates one menu item for each column of the underlying DataGrid to toggle that column's visibility
    private IEnumerable<FrameworkElement> CreateMenuItemsFor(DataGridColumnHeader columnHeader)
    {
        foreach (var column in _AssociatedObject.Columns)
        {
            var item = new MenuItem();
            item.Header = String.Format("Toggle visibility for {0}", column.Header);
            item.Command = ToggleColumnVisibilityCmd;
            item.CommandParameter = column;

            yield return item;
        }
    }

    // Gets executed when the user clicks on one of the ContextMenu items
    protected void ToggleColumnVisibilityCmdExecute(DataGridColumn column)
    {
        bool isVisible = (column.Visibility == Visibility.Visible);
        Visibility newVisibility = (isVisible) ? Visibility.Hidden : Visibility.Visible;
        column.Visibility = newVisibility;
    }
}

The SafeFindDescendants extension method is heavily based on the one from here: DataGridColumnHeader ContextMenu programmatically

public static class Visual_ExtensionMethods
{
    public static IEnumerable<T> SafeFindDescendants<T>(this Visual @this, Predicate<T> predicate = null) where T : Visual
    {
        if (@this != null)
        {
            int childrenCount = VisualTreeHelper.GetChildrenCount(@this);
            for (int i = 0; i < childrenCount; i++)
            {
                var currentChild = VisualTreeHelper.GetChild(@this, i);

                var typedChild = currentChild as T;
                if (typedChild == null)
                {
                    var result = ((Visual)currentChild).SafeFindDescendants<T>(predicate);

                    foreach (var r in result)
                        yield return r;

                }
                else
                {
                    if (predicate == null || predicate(typedChild))
                    {
                        yield return typedChild;
                    }
                }
            }

        }
    }
}

I can't figure out what's going on. Why does the context menu seem to be removed after hiding/re-showing a column?!

Appreciate any ideas! Thanks.

2

2 Answers

0
votes

I've come up with a quick and dirty Fix. It works, but it isn't pretty. Maybe someone can think of a better solution.

Essentially, each time the visibility of a DataGridColumn item changes to hidden/collapsed, I retrieve its DataGridColumnHeader and store the associated context menu in a cache. And each time the visibility changes back to visible, I'm listening to the next DataGrid LayoutUpdated event (to ensure the visual tree has been built), retrieve the DataGridColumnHeader again - which will incoveniently be a different instance than the original one - and set its context menu to the cached one.

    protected IDictionary<DataGridColumn, ContextMenu> _CachedContextMenues = new Dictionary<DataGridColumn, ContextMenu>();

protected void ToggleColumnVisibilityCmdExecute(DataGridColumn column)
    {
        bool isVisible = (column.Visibility == Visibility.Visible);
        Visibility newVisibility = (isVisible) ? Visibility.Hidden : Visibility.Visible;


        if(newVisibility != Visibility.Visible)
        {
            // We're hiding the column, so we'll cache its context menu so for re-use once the column
            // becomes visible again

            var contextMenu = _AssociatedObject.SafeFindDescendants<DataGridColumnHeader>(z => z.Column == column).Single().ContextMenu;
            _CachedContextMenues.Add(column, contextMenu);
        }
        if(newVisibility == Visibility.Visible)
        {
            // The column just turned visible again, so we set its context menu to the
            // previously cached one

            Observable
                .FromEventPattern(_AssociatedObject, "LayoutUpdated")
                .Take(1)
                .Select(x => _AssociatedObject.SafeFindDescendants<DataGridColumnHeader>(z => z.Column == column).Single())
                .Subscribe(x =>
                    {
                        var c = x.Column;
                        var cachedMenu = _CachedContextMenues[c];
                        _CachedContextMenues.Remove(c);
                        x.ContextMenu = cachedMenu;
                    });
        }

        column.Visibility = newVisibility;
    }
0
votes

Have you already found a better solution?

I have a new DataGrid class, so "this" is the actual Instance of a DataGrid! This is my solution (I'm also listen on the LayoutUpdated event):

this.LayoutUpdated += (sender, args) =>
{
    foreach (DataGridColumnHeader columnHeader in GetVisualChildCollection<DataGridColumnHeader>(this))
    {
        if(columnHeader.ContextMenu == null)
            ContextMenuService.SetContextMenu(columnHeader, _ContextMenu);
    }
};



    public static List<T> GetVisualChildCollection<T>(object parent) where T : Visual
    {
        List<T> visualCollection = new List<T>();
        GetVisualChildCollection(parent as DependencyObject, visualCollection);
        return visualCollection;
    }

    private static void GetVisualChildCollection<T>(DependencyObject parent, List<T> visualCollection) where T : Visual
    {
        int count = VisualTreeHelper.GetChildrenCount(parent);
        for (int i = 0; i < count; i++)
        {
            DependencyObject child = VisualTreeHelper.GetChild(parent, i);
            if (child is T)
            {
                visualCollection.Add(child as T);
            }
            else if (child != null)
            {
                GetVisualChildCollection(child, visualCollection);
            }
        }
    }