Silverlight: DataGrid with integrated field (column) chooser

Not much to say here, it speaks for itself.

Right click on a column header to open the field (columns) chooser and show/hide columns.

The VisibleColumns property is always up-to-date and can be binded TwoWay, in my case I am saving it to the user preferences for consistency.

Take a look at the code to see how it’s done.

Notice: ContextMenu causes memory leak! http://silverlight.codeplex.com/workitem/7089. as long as you don’t need to remove the DataGrid you are OK.

Demo is Here
Source is here

the WPF version is over here

13 Responses to Silverlight: DataGrid with integrated field (column) chooser

  1. danies8 says:

    tried to convert your implemnation to WPF platform.
    But, I get a null on the following row (GetTemplateChild) , why ?

    DataGridColumnHeadersPresenter headersPresenter = GetTemplateChild(“PART_FillerColumnHeader”)
    as DataGridColumnHeadersPresenter;

  2. danies8 says:

    #region Using
    using System;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Media.Imaging;
    using System.Linq;
    using System.Collections.Generic;
    using Microsoft.Windows.Controls;
    using Microsoft.Windows.Controls.Primitives;
    #endregion

    namespace MyProject.DataGridToolkit.Controls
    {
    ///
    /// DataGrid with integrated field chooser.
    /// Took from :https://shemesh.wordpress.com/category/silverlight/
    /// Right click on a column header to open the field chooser
    /// and show/hide columns.
    /// The VisibleColumns property is always up-to-date and can
    /// be binded Two ways.
    /// Im my case, I’m saving it to user prefereces fo consistency.
    ///
    public class DataGridExtended : DataGrid
    {

    #region Fields
    private ContextMenu _theContextMenu; //context menu for the field chooser.
    private bool _oneTime = true;
    #endregion

    #region Constrctor
    public DataGridExtended()
    : base()
    {
    _theContextMenu = new ContextMenu();
    Columns.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(Columns_CollectionChanged);
    }

    #endregion

    #region Properties

    #region AllColumnsHeaders
    private string AllColumnsHeaders { get; set; }
    #endregion

    #endregion

    #region VisibleColumns (DependencyProperty)

    ///
    /// Gets or sets a value indicating the names of columns (as they appear in the column header)
    /// to be visible, seperated by a semicolon.
    /// columns that whose name is not here will be hidden.
    ///
    public string VisibleColumns
    {
    get { return (string)GetValue(VisibleColumnsProperty); }
    set { SetValue(VisibleColumnsProperty, value); }
    }

    public static readonly DependencyProperty VisibleColumnsProperty =
    DependencyProperty.Register(
    “VisibleColumns”,
    typeof(string),
    typeof(DataGridExtended),
    new PropertyMetadata(“”, new PropertyChangedCallback(OnVisibleColumnsChanged))
    );

    private static void OnVisibleColumnsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
    DataGridExtended dg = (DataGridExtended)d;
    dg.VisibleColumnsChanged(e);

    }

    private void VisibleColumnsChanged(DependencyPropertyChangedEventArgs e)
    {
    if (e.NewValue != null)
    {
    string[] showTheseColumns = e.NewValue.ToString().Split(‘;’);
    string colName;

    // Update the columns visibility.
    foreach (DataGridColumn col in this.Columns)
    {
    colName = col.Header.ToString();

    if (showTheseColumns.Contains(colName))
    col.Visibility = Visibility.Visible;
    else
    col.Visibility = Visibility.Collapsed;
    }

    // update the context menu items.
    if (_theContextMenu != null)
    {
    foreach (MenuItem menuItem in _theContextMenu.Items)
    {
    colName = menuItem.Header.ToString();
    if (showTheseColumns.Contains(colName))
    ((Image)menuItem.Icon).Visibility = Visibility.Visible;
    else
    ((Image)menuItem.Icon).Visibility = Visibility.Collapsed;
    }
    }
    }
    }

    #endregion

    #region Methods

    #region ArrangeOverride
    ///
    /// FrameworkElement.ArrangeOverride Method
    /// =======================================
    /// When overridden in a derived class,
    /// positions child elements and determines a size for a FrameworkElement derived class.
    ///
    /// The final area within the parent
    /// that this element should use to arrange itself and its children
    /// The actual size used
    ///
    /// Notes to Inheritors:
    /// Control authors who want to customize the arrange pass of layout processing
    /// should override this method.
    /// The implementation pattern should call Arrange on each visible child element,
    /// and pass the final desired size for each child element as the finalRect parameter.
    /// Parent elements should call Arrange on each child,
    /// otherwise the child elements will not be rendered.
    protected override Size ArrangeOverride(Size finalSize)
    {

    if (_oneTime)// do this only once.
    {
    _oneTime = false;

    // Attach the context menu.
    // DataGridColumnHeadersPresenter headersPresenter = GetTemplateChild(“ColumnHeadersPresenter”) as DataGridColumnHeadersPresenter;
    DataGridColumnHeadersPresenter headersPresenter = GetTemplateChild(“PART_FillerColumnHeader”) as DataGridColumnHeadersPresenter;

    if (headersPresenter != null)
    {
    ContextMenuService.SetContextMenu(headersPresenter, _theContextMenu);

    // Update VisibleColumns as necessary.
    if (String.IsNullOrEmpty(VisibleColumns))
    VisibleColumns = AllColumnsHeaders;
    else
    {
    string s = VisibleColumns;
    VisibleColumns = null;
    VisibleColumns = s;
    }
    }
    }

    return base.ArrangeOverride(finalSize);
    }
    #endregion

    #endregion

    #region Events

    #region MenuItem_Click
    private void MenuItem_Click(object sender, RoutedEventArgs e)
    {
    MenuItem mi = sender as MenuItem;
    Image icon = (Image)mi.Icon;
    List splited = VisibleColumns.Split(‘;’).ToList();
    string colName = mi.Header.ToString();

    // Remove empty items.
    for (int i = 0; i 1)
    splited.Remove(colName);
    }

    // Update the VisibleColumns.
    string build = “”;
    foreach (string name in splited)
    {
    build = string.Format(“{0};{1}”, name, build);
    }
    VisibleColumns = build;
    }
    #endregion

    #region Columns_CollectionChanged
    private void Columns_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
    DataGridColumn col = e.NewItems[0] as DataGridColumn;

    // Keep a list of all clomuns headers for later use.
    AllColumnsHeaders = String.Format(“{0};{1}”, col.Header.ToString(), AllColumnsHeaders);

    // Make a new menu item and add it to the context menu.
    MenuItem menuItem = new MenuItem();
    Image img;
    menuItem.Click += new RoutedEventHandler(MenuItem_Click);
    menuItem.Header = col.Header.ToString();
    img = new Image();
    img.Source = Application.Current.Resources[“_vMark”] as BitmapImage;
    menuItem.Icon = img;
    _theContextMenu.Items.Add(menuItem);

    }
    #endregion

    #endregion

    }
    }

  3. danies8 says:

    Please look on ArrangeOverride method
    protected override Size ArrangeOverride(Size finalSize)

  4. danies8 says:

    For this lines rows:
    if (headersPresenter != null)
    {
    ContextMenuService.SetContextMenu(headersPresenter, _theContextMenu);

    …..

    • shemesh says:

      ok, i haven’t looked into wpf DataGrid template, your code assuming there is a inner control inside the DataGrid with x:Name of “PART_FillerColumnHeader”. does it?

  5. danies8 says:

    namespace Microsoft.Windows.Controls.Primitives
    {
    [TemplatePart(Name = “PART_FillerColumnHeader”, Type = typeof(DataGridColumnHeader))]
    public class DataGridColumnHeadersPresenter : ItemsControl
    {
    public DataGridColumnHeadersPresenter();

    • shemesh says:

      i m guessing here, for a real answer i’ll need some time.

      your derive from DataGrid therefor you have to look for that part on the DataGrid class, the code here is from some other class

  6. danies8 says:

    Please look on this sample:
    http://www.4shared.com/file/yr5fGtJT/1_online.html
    in vs2008
    add ref to WPFToolkit

  7. danies8 says:

    Here is the fix:

    I looked on the structure of the grid
    and use Visul Tree to find the DataGridColumnHeadersPresenter dp.
    And it found using debuuging.

    // attach the context menu.
    // =========================

    // In SL – works

    // DataGridColumnHeadersPresenter headersPresenter = GetTemplateChild(“ColumnHeadersPresenter”) as DataGridColumnHeadersPresenter;

    // In WPF – does not works

    //DataGridColumnHeadersPresenter headersPresenter = GetTemplateChild(“PART_FillerColumnHeader”) as DataGridColumnHeadersPresenter;

    New code:

    Border border = this.GetVisualChild(0) as Border;

    ScrollViewer scrollViewer = border.Child as ScrollViewer;

    Grid grid = VisualTreeHelper.GetChild(scrollViewer, 0) as Grid;

    UIElementCollection children = grid.Children;

    DataGridColumnHeadersPresenter headersPresenter = children[1] as DataGridColumnHeadersPresenter;

    If I understood the problem:

    GetTemplateChild behave deferently in SL and WPF.
    or

    A template part doesn’t have to be in the template. It can be omitted in most cases.

  8. shemesh says:

    i have posted a WPF version of this class over here:

    WPF: DataGrid with integrated field (column) chooser

    better implementation search by type, not by name or structure.

Leave a reply to shemesh Cancel reply