Creating Custom Columns for xamGrid

[Infragistics] Devin Rader / Thursday, July 08, 2010

When we originally sat down to design the xamGrid one of the problems we knew we wanted to address (based on lessons learned from building other grid controls) was providing an easy way for developers to extend the control by creating their own custom column types.  We knew that in the initial versions of the grid we were not going to be able to provide all of the column types we wanted to (the grid now includes seven different column types) and even long term there was no way we would be able to predict all of the different kinds of columns that developers would need for their applications, so we spent a lot of time designing an API that makes creating custom column types easy.

In this article I will show you how you can use the grids API to create custom columns for the xamGrid.  I will start by creating a simple non-editable column containing a Button control, then demonstrate a more advanced editable column that uses a ComboBox as its editor control.  The source code for the article is linked at the end.

Setting up a Custom Column

To get started creating a custom column I can start by setting up the basic outline of the classes I will need.  Note that although the outline shown here is specific to my first custom column type, the ButtonColumn, I will this same basic outline for any new column type I create.

The first thing I need to do is create a new class derived from the ColumnContentProviderBase class.  The ColumnContentProviderBase exposes a  number of virtual methods that the grid calls when it needs IU content for a columns cells or needs to resolve a value from an editable cell.  For now I am simply going to implement the method overrides and will come back later to add the method logic for my custom columns.

public class ButtonColumnContentProvider : ColumnContentProviderBase
{
    public override FrameworkElement 
        ResolveDisplayElement(Cell cell, Binding cellBinding)
    {
        throw new NotImplementedException();
    }

    protected override FrameworkElement ResolveEditorControl
        (Cell cell, object editorValue, double availableWidth, 
         double availableHeight, Binding editorBinding)
    {
        throw new NotImplementedException();
    }

    public override object ResolveValueFromEditor(Cell cell)
    {
        throw new NotImplementedException();
    }
}

Next, I need to add another class that derives from the Column class.  Classes derived from Column (or EditableColumn as you will see later) are ultimately what are added to grids Columns collection.  The Column class has one virtual method that I have to override called GenerateContentProvider which is used to connect the Column and ControlContentProvider together. 

To implement this method, all I need to do is instantiate and return an instance of my content provider.

public class ButtonColumn : Column
{
    protected override ColumnContentProviderBase GenerateContentProvider()
    {
        return new ButtonColumnContentProvider();
    }
}

Creating the ButtonColumn

Now that I have the basic outline set up, I can get on with implementing the logic for my simple ButtonColumn.  The ButtonColumn inserts a Button control into the columns cells, allows me to assign or bind the Buttons content and includes a Click event I can hook onto that provides me the Buttons row index and row data.

Configuring the Columns Public Properties and Events

The first step in creating this column is to add Content and ContentBinding dependency properties to the ButtonColumn class so that I can provide the content for the Button.  As noted earlier, because the ButtonColumn class is what I will eventually add to the grid Columns collections, any configuration properties I want my column to have should be exposed by this class.

public static readonly DependencyProperty ContentProperty =
    DependencyProperty.Register("Content", typeof(string), 
        typeof(ButtonColumn), 
        new PropertyMetadata(
            new PropertyChangedCallback(ContentChanged)));

public string Content
{
    get { return (string)this.GetValue(ContentProperty); }
    set { this.SetValue(ContentProperty, value); }
}
private static void ContentChanged(DependencyObject obj, 
                   DependencyPropertyChangedEventArgs e)
{
    ButtonColumn col = (ButtonColumn)obj;
    col.OnPropertyChanged("Content");
}

public static readonly DependencyProperty ContentBindingProperty = 
    DependencyProperty.Register("ContentBinding", 
        typeof(Binding), typeof(ButtonColumn), 
        new PropertyMetadata(
            new PropertyChangedCallback(ContentBindingChanged)));

public Binding ContentBinding
{
    get { return (Binding)this.GetValue(ContentBindingProperty); }
    set { this.SetValue(ContentBindingProperty, value); }
}

private static void ContentBindingChanged(DependencyObject obj,
    DependencyPropertyChangedEventArgs e)
{
    ButtonColumn col = (ButtonColumn)obj;
    col.OnPropertyChanged("ContentBinding");
}

Next I need to add a Click event to the ButtonColumn class so that I can be notified when a Button the the column is clicked:

public event EventHandler Click; protected internal void OnClick(ButtonColumnClickEventArgs e) { if (Click != null) { this.Click(this, e); } }

Notice that the Click event does not return the standard EventArgs object, but instead returns a custom event arguments object called ButtonColumnClickEventArgs.   This custom object provides me with the buttons row index and row data.

public class ButtonColumnClickEventArgs : EventArgs
{
    int _index;
    object _data;
    
    internal ButtonColumnClickEventArgs(int index, object data)
    {
        _index = index;
        _data = data;
    }

    public int Index { get { return _index; } }
    public object Data { get { return _data; } }
}

Finally, I have one other addition to make to the ButtonColumn.  By default all columns in the Grid expect their Key property to be set to real property of the grids items source, and will throw an exception if the Key cannot be found in the items source.  That does not really make sense for the ButtonColumn, so I want to change it so that the column does not expect the Key to exist in the items source.  I can do this by overriding the RequiresBoundDataKey property and returning false:

protected override bool RequiresBoundDataKey
{
    get
    {
        return false;
    }
}

That's all I have to do in the ButtonColumn class.  Now I can go back to the content provider class and add the logic that inserts the Button into the columns cells. 

Creating the Columns Display Element

To start writing the logic that actually shows the Button in the column and raises the Click event I first need to create an instance of a Button object in the constructor and wire up the Buttons click event to a new event handler method:

Button _button;

public ButtonColumnContentProvider()
{
    _button = new Button();
    _button.Click += new RoutedEventHandler(button_Click);
}

To tell the grid to us the Button as the content of the columns cells, I simply return the Button from the ResolveDisplayElement method:

public override FrameworkElement ResolveDisplayElement
    (Cell cell, Binding cellBinding)
{
    return _button;
}

ResolveDisplayElement is called by the grid any time it needs to create instantiate new UI content to show in a cell.  For example, if when your grid initially loads its height allows your column to display ten rows, then the ResolveDisplayElement method will be called ten times.  As long as the grids height never increases, ResolveDisplayElement won’t be called again.  If the grids height increases so that five additional rows become visible in the column, then ResolveDisplayElement will be called five more times in order to create the content for those five additional cells. 

If I went ahead and added this column to a grid now, the Button would be displayed, but would have not any content and clicking it would do nothing.  To place content in to the Button, I need to add some code to the content provider class that uses the Content and ContentBinding properties I created in the ButtonColumn above.  The snippet below shows how to add this code to the ResolveDisplayElement method:

public override FrameworkElement ResolveDisplayElement
    (Infragistics.Silverlight.Controls.Cell cell, 
     System.Windows.Data.Binding cellBinding)
{
    _column = (ButtonColumn)cell.Column;

    Binding binding = new Binding(_column.Key);
    if (_column.Content == null)
    {
        binding.Mode = BindingMode.OneWay;
        if (_column.ContentBinding != null)
        {
            this._button.SetBinding(Button.ContentProperty, 
                                    _column.ContentBinding);
        }
        else
        {
            this._button.SetBinding(Button.ContentProperty, binding);
        }
    }
    else
    {
        binding = new Binding("Content");
        binding.Source = _column;
        this._button.SetBinding(Button.ContentProperty, binding);
    }

    return _button;
}

As you can see, I am basically checking to see if the Content property is null, and if it is trying to use the ContentBinding property.  If The ContentBinding property itself is null then I fallback to binding to the columns key.  If the Content property is not null, I use that value.

Finally I need to raise the columns Click event when a Button in one of its cells is clicked.  As part of the Click event I want to return my custom event arguments object that contains the index of the row that contains the clicked button, as well as the data from that row, but because of  how the grid recycles UI elements I need to make sure that I am returning the right data in my event arguments. To do this I override the AdjustDisplayElement method which, unlike the ResolveDisplayElement method which is called infrequently by the grid, is called whenever a measure is called by the layout cycle.  This method is useful for making minor tweaks to a control in a particular cell.

In order to make sure that I am returning the correct data in my click event arguments, I store the cell parameter off into a local member in the AdjustDisplayElement method.

public override void AdjustDisplayElement(Infragistics.Controls.Grids.Cell cell)
{
    this._cell = cell;

    base.AdjustDisplayElement(cell);
}

Now I can add the code to create a new instance of ButtonColumnClickEventArgs and raise the columns public Click event to the button click handler I created earlier:

void button_Click(object sender, RoutedEventArgs e)
{
    ButtonColumnClickEventArgs _clickeventargs = 
        new ButtonColumnClickEventArgs(_cell.Row.Index, _cell.Row.Data);
    _column.OnClick(_clickeventargs);
}

That's all need to do to create the ButtonColumn.  To use it in a xamGrid, I simply add it to my grids columns collection:

     

Clicking the buttons in the column adds a line the the Textbox indicating the row index of the button and the object type of the row.

Creating an editable ComboBoxColumn

The second type of custom column I want to create is an editable column that uses a ComboBox control as its value editor.  To do this I can return to the basic custom column framework I shows at the beginning of the article, and making one small change to the column class.  Instead of deriving from Column, I want the class to derive from the EditableColumn class.  This informs the grid that this column is editable and allows it to participate in all of the standard cell editing events exposed by the grid.

public class ComboBoxColumn : EditableColumn
{
    protected override ColumnContentProviderBase GenerateContentProvider()
    {
        return new ComboBoxColumnContentProvider();
    }
}

Now I can follow the same process I used for creating the ButtonColumn, first setting up all of the properties I want to expose from my Column then returning to the content provider class to set up the display element, and in this case also the editor control.

Configuring the Column Properties

For the ComboBoxColumn, the properties exposed by the column are all related to setting up the internal ComboBox that I am going to use as the cells editor, including an ItemSource, DisplayMemberPath and an ItemsTemplate if I want to customize the look of the ComboBox items.

public static readonly DependencyProperty ItemSourceProperty = 
    DependencyProperty.Register("ItemSource", 
        typeof(IEnumerable), 
        typeof(ComboBoxColumn), 
        new PropertyMetadata(
            new PropertyChangedCallback(ItemSourceChanged)));

public IEnumerable ItemSource
{
    get { return (IEnumerable)this.GetValue(ItemSourceProperty); }
    set { this.SetValue(ItemSourceProperty, value); }
}

private static void ItemSourceChanged(DependencyObject obj, 
    DependencyPropertyChangedEventArgs e)
{
    ComboBoxColumn col = (ComboBoxColumn)obj;
    col.OnPropertyChanged("ItemSource");
}

public static readonly DependencyProperty DisplayMemberPathProperty = 
    DependencyProperty.Register("DisplayMemberPath", 
        typeof(string), 
        typeof(ComboBoxColumn), 
        new PropertyMetadata(
            new PropertyChangedCallback(DisplayMemberPathChanged)));

public string DisplayMemberPath
{
    get { return (string)this.GetValue(DisplayMemberPathProperty); }
    set { this.SetValue(DisplayMemberPathProperty, value); }
}

private static void DisplayMemberPathChanged(DependencyObject obj, 
    DependencyPropertyChangedEventArgs e)
{
    ComboBoxColumn col = (ComboBoxColumn)obj;
    col.OnPropertyChanged("DisplayMemberPath");
}

public static readonly DependencyProperty ItemTemplateProperty = 
    DependencyProperty.Register("ItemTemplate", 
        typeof(DataTemplate), 
        typeof(ComboBoxColumn), 
        new PropertyMetadata(
            new PropertyChangedCallback(ItemTemplateChanged)));

public string ItemTemplate
{
    get { return (string)this.GetValue(ItemTemplateProperty); }
    set { this.SetValue(ItemTemplateProperty, value); }
}

private static void ItemTemplateChanged(DependencyObject obj, 
    DependencyPropertyChangedEventArgs e)
{
    ComboBoxColumn col = (ComboBoxColumn)obj;
    col.OnPropertyChanged("ItemTemplate");
}

That's it for the Column configuration.  Now I can return to the content provider to finish this column.

Creating the Columns Display Element and Editor Control

I know that for the ComboBoxColumn I want to use a simple TextBlock as the columns DisplayElement and a ComboBox for the control editor, so the first thing I will do is create instances these controls and return them from the appropriate methods:

public class ComboBoxColumnContentProvider : ColumnContentProviderBase
{
    TextBlock _tb;
    ComboBox _combobox;

    public ComboBoxColumnContentProvider()
    {
        _tb = new TextBlock();
        _combobox = new ComboBox();
    }

    public override FrameworkElement ResolveDisplayElement
        (Infragistics.Silverlight.Controls.Cell cell, 
         System.Windows.Data.Binding cellBinding)
    {
        return _tb;
    }

    protected override FrameworkElement ResolveEditorControl
        (Infragistics.Silverlight.Controls.Cell cell, 
         object editorValue, double availableWidth, 
         double availableHeight, System.Windows.Data.Binding editorBinding)
    {
        return this._combobox;
    }

    public override object ResolveValueFromEditor
        (Infragistics.Silverlight.Controls.Cell cell)
    {
        return null;

    }
}

Now that I have the controls created, much like i did in the ButtonColumn, I need to bind the TextBlock to the cells value using a Binding.

public override FrameworkElement ResolveDisplayElement
    (Infragistics.Silverlight.Controls.Cell cell, 
     System.Windows.Data.Binding cellBinding)
{
    ComboBoxColumn column = (ComboBoxColumn)cell.Column;

    Binding textBinding = new Binding();
    textBinding.Source = cell.Row.Data;

    if (column.DisplayMemberPath!=null)
        textBinding.Path = 
            new PropertyPath(string.Format("{0}.{1}",column.Key, 
                             column.DisplayMemberPath));
    else
        textBinding.Path = new PropertyPath(column.Key);

    textBinding.Mode = BindingMode.TwoWay;
    this._tb.SetBinding(TextBlock.TextProperty, textBinding);

    return _tb;
}

As you can see from the snippet above, when the columns DisplayMemberPath property is set, I create a new PropertyPath for my Binding object that combines the columns Key with the DisplayMemberPath and set that as the Binding objects Path.  Once the path created, I can assign the Binding to the TextBlocks Text property.

Now that the display element is set up, I can start to create the editing experience for this column using a ComboBox and the ResolveEditorControl method.  The process for doing this is similar to setting up the display editor.  Basically I create an instance of the editor control and then set up a binding to bind it to the cell data.  In this case because the ComboBox is itself a bound control, I also have to connect it to a data source and set up its selected item property.

ComboBoxColumn column = (ComboBoxColumn)cell.Column;

this._combobox.SetValue(ComboBox.ItemsSourceProperty, column.ItemSource);
this._combobox.SetValue(ComboBox.DisplayMemberPathProperty, 
                        column.DisplayMemberPath);

Binding selectedItemBinding = new Binding();
selectedItemBinding.Source = cell.Row.Data;
selectedItemBinding.Path = new PropertyPath(column.Key);
selectedItemBinding.Mode = BindingMode.TwoWay;

this._combobox.SetBinding(ComboBox.SelectedItemProperty, selectedItemBinding);

this._combobox.Style = cell.EditorStyleResolved;

return this._combobox;

As shown in snippet above I first assign values from my columns ItemSource and DisplayMemberPath properties to the corresponding ComboBox columns.  Once that is done I create a new binding called selectedItemBinding to bind the cells value to the ComboBoxes SelectedItem property.  What's especially important to note here is that the Mode of the Binding has been set to TwoWay.  This means that when the ComboBox initially loads the correct item will be selected by default, and if the SelectedItem changes in the ComboBox, the cell value will automatically be updated with this change without me having to listen to the ComboBoxes SelectedItemChanged event.

The last thing I have to do is use the content providers ResolveValueFromEditor method to tell the grid how to get the selected value from the ComboBox, which it will assign as the cells value.  I do this by returning the ComboBoxes SelectedItem property form that method:

public override object ResolveValueFromEditor
    (Infragistics.Silverlight.Controls.Cell cell)
{
    return this._combobox.SelectedItem;
}

That's all there is to creating a custom ComboBoxEditor column using the grids API.  If I load this column in grid, when the cell is in display mode the cell value is shown in the TextBlock is show.  Putting the cell into edit mode shows the ComboBox with its bound values and the cell value selected by default.

 

Conclusion

After reading this article you can see how easy it is to extend the xamGrid by creating your own custom column types.  The source code for the examples shown can be found linked below.  Note that in order to compile the samples as-is, you will need to have the Silverlight controls from the latest versions of of NetAdvantage for WebClient: Silverlight installed.