(X) Hide this
    • Login
    • Join
      • Generate New Image
        By clicking 'Register' you accept the terms of use .

How to inherit from ItemsControl and create a UniformGrid with containers

(2 votes)
Denislav Savkov
>
Denislav Savkov
Joined Feb 11, 2008
Articles:   14
Comments:   6
More Articles
8 comments   /   posted on Jul 28, 2008
Categories:   Controls

This article is compatible with the latest version of Silverlight.

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. 

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.
 protected override void PrepareContainerForItemOverride( DependencyObject element, object item )
 {
     base.PrepareContainerForItemOverride( element, item );
     ItemContainer item2 = element as ItemContainer;
     bool flag = true;
     if ( item2 != item )
     {
         if ( base.ItemTemplate != null )
         {
              item2.ContentTemplate = base.ItemTemplate;
          }
          else if ( !string.IsNullOrEmpty( base.DisplayMemberPath ) )
          {
              Binding binding = new Binding( base.DisplayMemberPath );
              item2.SetBinding( ContentControl.ContentProperty, binding );
              flag = false;
          }
          if ( flag )
          {
              item2.Content = item;
          }
          // Addition to the original ListBox function that we use
          this.ArrangeItem( item2 );
   
          this.ObjectToItemContainer[ item ] = item2;
      }
      if ( ( this.ItemContainerStyle != null ) && ( item2.Style == null ) )
      {
          item2.Style = this.ItemContainerStyle;
      }
  }

 

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:

internal virtual void OnItemContainerStyleChanged( Style oldItemContainerStyle, Style newItemContainerStyle )
{
    if ( oldItemContainerStyle != newItemContainerStyle )
        foreach ( object obj2 in base.Items )
        {
            ItemContainer ItemContainerForObject = this.GetItemContainerForObject( obj2 );
            if ( ( ItemContainerForObject != null ) && ( ( ItemContainerForObject.Style == null ) ||
 ( oldItemContainerStyle == ItemContainerForObject.Style ) ) )
            {
                if ( ItemContainerForObject.Style != null )
                {
                    throw new NotSupportedException( null );
                }
                ItemContainerForObject.Style = newItemContainerStyle;
            }
        }
}

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);
        if (ItemContainerForObject != null)
            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.

protected void GoToNextIndex()
{
       if (ChildrenFlow == Orientation.Horizontal)
       {
           if (_lastRowIndex >= Rows && AutoFill == true )
               Rows++;
           if (_lastColumnIndex < Columns - 1)
               _lastColumnIndex++;
           else
           {
               _lastColumnIndex = 0;
               _lastRowIndex++;
           }
}

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


Subscribe

Comments

  • -_-

    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
  • -_-

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


    posted by Beaudetious on Dec 17, 2009 05:00
    Excellent article.  There's a lot to digest but it's worth it. I'm looking forward to the next one.
  • -_-

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


    posted by Enrico Valiani on Jan 23, 2010 12:47
    Why does it ask me for install Microsoft Silverlight? I've already installed it (version 3)
  • iiordanov

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


    posted by iiordanov on Jan 24, 2010 12:00

    Hi Enrico Valiani,

    It is probably because this was developed for older version of Silverlight.

  • -_-

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


    posted by Ladi on Oct 04, 2010 13:57

    I struggled a lot with the subject of inheriting from ItemsControl. I wish I’ve seen this article a year ago. Since then I’ve did a lot of work around this topic including the aspect of virtualization. You can see all the data I collected on the subject at: http://www.ladimolnar.com/Home/ViewArticle.aspx?ArticleName=HowToCustomizeAnItemsControl. Also a sample app at http://www.ladimolnar.com/Projects/ItemsControlDemo.

     

  • lnikolov

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


    posted by lnikolov on Jan 13, 2011 12:22
    The article has been updated to the latest version of Silverlight and Visual Studio.
  • Re: How to inherit from ItemsControl and create a UniformGrid with containers


    posted by DerekEwing on Jun 03, 2012 23:18
    Hello, At the end of your post you mention that we could achieve the same result by inheriting from Panel rather than ItemsControl. Which looks like less code. If we do this we can no longer set the default style key in the constructor,nwhich we can do if we inherit from ItemsControl. If we did inherit from Panel how would we be able to skin the custom panel ?

Add Comment

Login to comment:
  *      *