Recommended


Silverlight Hosting

Skip Navigation LinksHome / Articles / View Article

How to inherit from ItemsControl and create a UniformGrid with containers

+ Add to SilverlightShow Favorites
2 comments   /   posted by Denislav Savkov on Jul 28, 2008
(0 votes)
Categories: Demos , Learn , Tutorials , Samples

Introduction

In our serial article about custom controls in Silverlight we will create a control that inherits from ItemsControl. We thought it would be interesting to show you how to replace the default StackPanel with Grid and let items arrange consecutively like in StackPanel instead of using the Row and Column attached properties. Another thing we added is a container for each item. We used ListBox as a model for our control, the Reflector was very helpful for that. To achieve extended functionality we implement Dependency Properties. One of the advantages is that Visual Studio Designer is displaying our control correctly. Howerver we had some problems with the styling of our control. Maybe because of the beta state of Visual Studio SP1 and Silverlight there were some situations where VS crashed consistently. So while you experiment with our control it is possible to come upon such situation.

Download full source code

Overview

What we do is:

    • inherit from ItemsControl
    • in the default template of our control we change the ItemsPanel to Grid
    • override some ItemsControl methods to support custom container
    • add custom logic to arrange the Grid and the items in it

The first two steps are pretty self-explanatory. To see the basics of templating and how templating combines with the VisualStateManager see the following links.

Creating a Silverlight Custom Control - The Basics 

Custom ContentControl using VisualStateManager

Inheriting from ItemsControl

To be able to use a container for the objects in the Grid we need to override some protected methods from ItemsControl. This will allow the use of a single template for all objects we put into the grid. 

The first addition to the ItemsControl that we inherit is a dictionary where we keep the items and their corresponding containers:

private Dictionary<object, ItemContainer> _objectToItemContainer;
 
and two helper methods
 
private ItemContainer GetItemContainerForObject( object value )
{
    ItemContainer item = value as ItemContainer;
    if ( item == null )
    {
        this.ObjectToItemContainer.TryGetValue( value, out item );
    }
    return item;
}
 
private IDictionary<object, ItemContainer> ObjectToItemContainer
{
    get
    {
        if ( this._objectToItemContainer == null )
        {
            this._objectToItemContainer = new Dictionary<object, ItemContainer>();
        }
        return this._objectToItemContainer;
    }
}
 
 
The table below explains more about the methods we override. 
 
Name Description from MSDN
GetContainerForItemOverride Creates or identifies the element that is used to display the given item.
Implementation
Returns an instance of the container.
protected override DependencyObject GetContainerForItemOverride()
{
    ItemContainer item = new ItemContainer();
    if ( this.ItemContainerStyle != null )
    {
        item.Style = this.ItemContainerStyle;
    }
    return item;
}

 

Name Description from MSDN
IsItemItsOwnContainerOverride Determines if the specified item is (or is eligible to be) its own container.
Implementation
Returns true if the item is of the type of the container.
protected override bool IsItemItsOwnContainerOverride(object item)
{
    return (item is ItemContainer);
}

 

Name Description from MSDN
PrepareContainerForItemOverride Prepares the specified element to display the specified item.
Implementation
Applies ItemTemplate (which is DataTemplate) to the item. Sets the content of the container to be the item. Applies Style to the container. Mainains the index of the last added item.
   1:protected override void PrepareContainerForItemOverride( DependencyObject element, object item )
   2: {
   3:     base.PrepareContainerForItemOverride( element, item );
   4:     ItemContainer item2 = element as ItemContainer;
   5:     bool flag = true;
   6:     if ( item2 != item )
   7:     {
   8:         if ( base.ItemTemplate != null )
   9:         {
  10:             item2.ContentTemplate = base.ItemTemplate;
  11:         }
  12:         else if ( !string.IsNullOrEmpty( base.DisplayMemberPath ) )
  13:         {
  14:             Binding binding = new Binding( base.DisplayMemberPath );
  15:             item2.SetBinding( ContentControl.ContentProperty, binding );
  16:             flag = false;
  17:         }
  18:         if ( flag )
  19:         {
  20:             item2.Content = item;
  21:         }
  22:         // Addition to the original ListBox function that we use
  23:         this.ArrangeItem( item2 );
  24:  
  25:         this.ObjectToItemContainer[ item ] = item2;
  26:     }
  27:     if ( ( this.ItemContainerStyle != null ) && ( item2.Style == null ) )
  28:     {
  29:         item2.Style = this.ItemContainerStyle;
  30:     }
  31: }

 

Name Description from MSDN
ClearContainerForItemOverride When overridden in a derived class, undoes the effects of the PrepareContainerForItemOverride method.
Implementation
Removes the item if it is not self container. Corrects the index of the last item.
protected override void ClearContainerForItemOverride( DependencyObject element, object item )
{
    base.ClearContainerForItemOverride( element, item );
    ItemContainer item2 = element as ItemContainer;
    if (item == null)
    {
        item = (item2.Content == null) ? item2 : item2.Content;
    }
    if (item2 != item)
    {
        this.ObjectToItemContainer.Remove(item);
        GoToPreviousIndex();
    }
}

We've taken the implementations of these functions from ListBox and modified them to suit our purposes. This way we have similar functionality. The main difference with the ListBox functions is that we add the ArrangeItem method to put the item in the correct column and row of the Grid.

As we can see from the names of the functions ItemsControl assumes the inheritor class will have a container. In our example the class we use as a container is ItemsContainer which is simply an empty ContentControl inheritor. We did that in case we want a default style for the container. To be able to apply custom style to the container we expose ItemContainerStyle property. This is a dependency property and when it's changed we iterate through our dictionary to change the style of each container:

   1: internal virtual void OnItemContainerStyleChanged( Style oldItemContainerStyle, Style newItemContainerStyle )
   2: {
   3:     if ( oldItemContainerStyle != newItemContainerStyle )
   4:         foreach ( object obj2 in base.Items )
   5:         {
   6:             ItemContainer ItemContainerForObject = this.GetItemContainerForObject( obj2 );
   7:             if ( ( ItemContainerForObject != null ) && ( ( ItemContainerForObject.Style == null ) || 
   8:                                                 ( oldItemContainerStyle == ItemContainerForObject.Style ) ) )
   9:             {
  10:                 if ( ItemContainerForObject.Style != null )
  11:                 {
  12:                     throw new NotSupportedException( null );
  13:                 }
  14:                 ItemContainerForObject.Style = newItemContainerStyle;
  15:             }
  16:         }
  17: }

So far we exploited code from the ListBox control in Silverlight.

Logic of arrangement

Our control has uniform cells. This eliminates the need of Row and ColumnDefinitions from the user. We have two properties - Rows and Columns instead. When they are changed the control adds the appropriate number of definitions to the grid. However we had troubles getting reference to the Grid of our control. To use grid we define a default style for our control. The easiest way to get access to certain control is to give it a name in the Template setter. However, ItemsPanel is a property outside of the Template property. So GetTemplateChild that we usually use for getting reference to a control in a template can't find the control defined in ItemsPanelTemplate. Because of that we get reference to the ItemsPanel element in the Template setter and then search its children to find a Grid.

<Style TargetType="c:UniformGrid">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate>
                <Grid Background="{TemplateBinding Background}">
                    <ItemsPresenter x:Name="ItemsPresenter">ItemsPresenter>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<Grid />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Style>

This approach had problems too. Applying the ItemsPanelTemplate seems to be the last thing that's done during the initialization of the control. That's our conclusion since the children of "ItemsPresenter" would be 0 even after the Loaded event or when OnApplyTemplate occurs. Our workaround is to hook to the first LayoutUpdate event which works fine.

public override void OnApplyTemplate()
{
    base.OnApplyTemplate();
 
    ItemsPresenter = (ItemsPresenter)GetTemplateChild("ItemsPresenter");// 0 children
    ItemsPresenter.LayoutUpdated += new EventHandler(ItemsPresenter_LayoutUpdated);
}
 
void ItemsPresenter_LayoutUpdated(object sender, EventArgs e)
{
    ItemsPresenter.LayoutUpdated -= new EventHandler(ItemsPresenter_LayoutUpdated);
    ItemsPresenter = ( ItemsPresenter )GetTemplateChild( "ItemsPresenter" );
    UpdateMeasure();
}

Now we add the columns and rows for the user in UpdateMeasure().

DependencyObject target = VisualTreeHelper.GetChild(ItemsPresenter, i);
if (target is Grid)
{
    g = target as Grid;
 
    g.RowDefinitions.Clear();
    for (int r = 0; r < Rows; r++)
        g.RowDefinitions.Add(new RowDefinition());
 
    g.ColumnDefinitions.Clear();
    for (int c = 0; c < Columns; c++)
        g.ColumnDefinitions.Add(new ColumnDefinition());
}

The other arrangement method iterates the items and updates their position in the grid. We keep the index of the last added item in _last*Index private members.

protected void ArrangePanel()
{
    _lastColumnIndex = 0;
    _lastRowIndex = 0;
    foreach (object obj in base.Items)
    {
        ItemContainer ItemContainerForObject = this.GetItemContainerForObject(obj);
        ArrangeItem(ItemContainerForObject);
    }
}
protected void ArrangeItem(ItemContainer item)
{
    item.SetValue(Grid.ColumnProperty, _lastColumnIndex);
    item.SetValue(Grid.RowProperty, _lastRowIndex);
    GoToNextIndex();
}

ArrangeItem() calls GoToNextItem to prepare the index for the next item.

   1: protected void GoToNextIndex()
   2: {
   3:     if (ChildrenFlow == Orientation.Horizontal)
   4:     {
   5:         if (_lastRowIndex >= Rows && AutoFill == true )
   6:             Rows++;
   7:         if (_lastColumnIndex < Columns - 1)
   8:             _lastColumnIndex++;
   9:         else
  10:         {
  11:             _lastColumnIndex = 0;
  12:             _lastRowIndex++;
  13:         }
  14:     }
  15:     ...

This illustrates the logic of arrangement when items are added horizontally. Simply move to the end column and then continue from the beginning of the next row. The logic for returning back the index when removing items is analogical but in the opposite direction. We've added two more properties. AutoFill when it's true allows the number of rows/columns to change dynamically when the control is full and we add more items. The other property ChildrenFlow says whether to fill the Grid row by row or column by column.

Conclusion

In the end we should point that there is a different approach to create such UniformGrid as we like to call it.  Instead of inheriting from ItemsControl and arranging the items in the Grid you could inherit from panel to create your own grid and override ArrangeOverride and MeasureOverride and then make it ItemsPanel of the ItemsControl.

The custom ItemsControl that we made offers functionality similar to that of a ListBox. You may bind it to data objects in ObservableCollection or you can define element directly in the XAML where you use it. We didn't include selection though it is probably good feature. We wanted to concentrate on consistency in this example to make truly usable control. Await our future article where we would probably use this control and have some selection included.

References

The Layout System

http://msdn.microsoft.com/en-us/library/ms745058.aspx

Custom Radial Panel Sample

http://msdn.microsoft.com/en-us/library/ms771363.aspx

ItemsControl class

http://msdn.microsoft.com/en-us/library/system.windows.controls.itemscontrol(VS.95).aspx

Share


Comments

Comments RSS RSS
  • RE: How to inherit from ItemsControl and create a UniformGrid with containers  

    posted by Necriis on Mar 24, 2009 04:58
    ItemsPresenter = (ItemsPresenter)GetTemplateChild("ItemsPresenter");// 0 children

    this line return null in ItemsPresenter
  • RE: How to inherit from ItemsControl and create a UniformGrid with containers  

    posted by Mark on Jun 08, 2009 02:37
    Create a new folder in the ClassLibrary. Rename it 'Themes'. Place the generic.xaml file into the new folder. Should solve the problem

Add Comment

 
 

   
  
  
   
 Refresh