IanG on Tap

Ian Griffiths in Weblog Form (RSS 2.0)

Blog Navigation

April (2018)

(1 item)

August (2014)

(1 item)

July (2014)

(5 items)

April (2014)

(1 item)

March (2014)

(1 item)

January (2014)

(2 items)

November (2013)

(2 items)

July (2013)

(4 items)

April (2013)

(1 item)

February (2013)

(6 items)

September (2011)

(2 items)

November (2010)

(4 items)

September (2010)

(1 item)

August (2010)

(4 items)

July (2010)

(2 items)

September (2009)

(1 item)

June (2009)

(1 item)

April (2009)

(1 item)

November (2008)

(1 item)

October (2008)

(1 item)

September (2008)

(1 item)

July (2008)

(1 item)

June (2008)

(1 item)

May (2008)

(2 items)

April (2008)

(2 items)

March (2008)

(5 items)

January (2008)

(3 items)

December (2007)

(1 item)

November (2007)

(1 item)

October (2007)

(1 item)

September (2007)

(3 items)

August (2007)

(1 item)

July (2007)

(1 item)

June (2007)

(2 items)

May (2007)

(8 items)

April (2007)

(2 items)

March (2007)

(7 items)

February (2007)

(2 items)

January (2007)

(2 items)

November (2006)

(1 item)

October (2006)

(2 items)

September (2006)

(1 item)

June (2006)

(2 items)

May (2006)

(4 items)

April (2006)

(1 item)

March (2006)

(5 items)

January (2006)

(1 item)

December (2005)

(3 items)

November (2005)

(2 items)

October (2005)

(2 items)

September (2005)

(8 items)

August (2005)

(7 items)

June (2005)

(3 items)

May (2005)

(7 items)

April (2005)

(6 items)

March (2005)

(1 item)

February (2005)

(2 items)

January (2005)

(5 items)

December (2004)

(5 items)

November (2004)

(7 items)

October (2004)

(3 items)

September (2004)

(7 items)

August (2004)

(16 items)

July (2004)

(10 items)

June (2004)

(27 items)

May (2004)

(15 items)

April (2004)

(15 items)

March (2004)

(13 items)

February (2004)

(16 items)

January (2004)

(15 items)

Blog Home

RSS 2.0

Writing

Programming C# 5.0

Programming WPF

Other Sites

Interact Software

ListView Column Margin

Wednesday 30 May, 2007, 10:44 PM

[Updated 31st May 2007: Dan Sullivan emailed me to point out a much simpler way to implement my hack.]

In my previous post, I showed how to set the alignment on a ListView column in order to make the column contents stretch to fill the available width. I also observed that it’s tricky to get rid of the margins to the left and right of the contents of each column. Let’s take a look at that.

The problem is that the ListView adds a fixed, hard-coded margin around each column item. It’s added by the GridViewRowPresenter class. You get one GridViewRowPresenter object for each row in the list, and its job is to generate the items for each of the columns in the list. Item generation happens in a private method called CreateCell, and if you take a look at this with a tool like Reflector, you’ll see that this does two slightly irritating things to the elements it generates:

The first is moderately irritating because it requires us to use the technique described in my previous post. (See that post for an explanation of why this causes a problem. The executive summary is that this ends up aligning column contents to the left, when you really want Stretch alignment.) Fortunately, this first problem is easy to work around, as described in the previous post.

The second problem—the hard-coded Margin—is much more irritating. It turns out that there’s simply no way of changing the margin. That _defalutMargin field is initialized during static construction to new Thickness(6,0,6,0) and there’s no mechanism for altering it.

I get the impression that the ListView didn’t get quite the same level of review as most of the other controls in WPF. This is compounded by the poor spelling of the _defalutMargin property. Of course bad spelling is not a reliable indicator of bad code, particularly if the developer’s first language is not English. However, since an awful lot of the people on the WPF team do seem to have English as a first language, the fact that this mistake wasn’t noticed does make me wonder just how many people reviewed this code.

But it’s not really about the spelling. The fundamental issue that hard-coding a Margin into the guts of some helpful function like this seems distinctly un-WPF. Visible features like margins and padding are almost invariably specified in either a Style or a Template, so it’s really unusual to see such a visual feature baked into the code. Most of the built-in controls allow pretty much everything you see about them to be replaced, but you can’t get rid of this column margin in the ListView. Even if you’re prepared to take the drastic step of completely re-templating the entire control just to fix this one thing, that doesn’t help. The GridViewRowPresenter provides some critical functionality of the grid view of the ListView. (It’s the thing that knows how to generate the items that make up a row in the list.) This means that you have to use a GridViewRowPresenter in a custom template for a ListViewItem if you want to use the grid view, which in turn means that you can’t get rid of the fixed margin. (Or at least, not without a horrible hack—see below.)

In short, GridViewRowPresenter inextricably combines core functionality of the control with an aspect of the control’s appearance. This is disappointing, because the vast majority of WPF controls separate such things.

I’m not easily discouraged however, so I invented a horrible workaround.

Horrible Hack of a Workaround

The big problem with the fact that GridViewRowPresenter just sets the Margin property directly instead of using a style is that local values for properties have a very high precedence. (Most properties in WPF have several potential values. There’s the default value, there might be a value provided by a style in the current theme, that style may also have triggers, there might be a local application style, the property might be animated, etc. WPF defines an order of precedence for these possible values. The current value is simply the highest priority value available.) So you can’t override these things with a style, because a local property values always take precedence over styles.

Perhaps we could provide a higher priority value. But there are only two value sources with higher priority than local values: animations, and coerced values. We could use coercion by overriding the metadata for Margin on a custom element. Problem is, WPF won’t actually use our custom element—GridViewRowPresenter always creates either a TextBlock or a ContentPresenter. We could try to animate the relevant element, but that seems pretty horrible, and in any case, it’s not obvious how you’d target the thing.

Instead, I went with a simpler hack: write a custom element that resets its parent’s Margin. [Update: this solution isn’t actually necessary. It has been pointed out to me that a simpler solution exists, described in the next section. So please don’t use this...] This is horrible, but fairly simple, and it seems to be effective. Here it is:

public class RemoveParentMargin : Decorator
{
    protected override void OnVisualParentChanged(DependencyObject oldParent)
    {
        base.OnVisualParentChanged(oldParent);

        FrameworkElement cp = VisualTreeHelper.GetParent(this)
                                 as FrameworkElement;
        if (cp != null)
        {
            cp.Margin = new Thickness(0.0);
        }
    }
}

We can now modify the example from my previous entry to use this:

<Grid>
  <Grid.Resources>
    <x:Array Type="{x:Type s:String}" x:Key="items">
      <s:String>Foo</s:String>
      <s:String>Bar</s:String>
      <s:String>Spong</s:String>
    </x:Array>
  </Grid.Resources>

  <ListView ItemsSource="{StaticResource items}">

    <ListView.ItemContainerStyle>
      <Style TargetType="ListViewItem">
        <Setter Property="HorizontalContentAlignment"
                Value="Stretch" />
      </Style>
    </ListView.ItemContainerStyle>

    <ListView.View>
      <GridView>
        <GridViewColumn Header="Data" Width="80">
          <GridViewColumn.CellTemplate>
            <DataTemplate>
              <loc:RemoveParentMargin>
                <TextBox Margin="0"  Text="{Binding .}"  />
              </loc:RemoveParentMargin>
            </DataTemplate>
          </GridViewColumn.CellTemplate>
        </GridViewColumn>
        <GridViewColumn Header="Length"
                        DisplayMemberBinding="{Binding Length}" />
      </GridView>
    </ListView.View>
  </ListView>

</Grid>

(As before, this assumes you’ve mapped the s: prefix to the System namespace in mscorlib. It also requires loc: to be mapped to whatever namespace in which you’ve defined that RemoveParentMargin class.)

The first column’s TextBox is now wrapped with this RemoveParentMargin hack element, which successfully removes the unwanted margin. The TextBox gets to fill the whole width of the column.

Update: After I posted this, Dan Sullivan pointed out to me that we get exactly this effect without needing to go as far as writing a custom element. A simpler solution exists.

A Simpler Horrible Hack

We don’t need the custom RemoveParentMargin to get rid of the margin in this particular example. We can just give the root element of the column template a negative margin:

<GridViewColumn.CellTemplate>
  <DataTemplate>
    <TextBox Margin="-6,0"  Text="{Binding .}"  />
  </DataTemplate>
</GridViewColumn.CellTemplate>

The reason this solution eluded me is that I was thinking that GridViewRowPresenter would clip its children. Not sure why I thought that, but there you are...

Yuck

I really don’t like either of these workarounds. They feel fragile. The first, because it depends on implementation details. Any change to how the ListView works could easily break it—this just assumes that the parent of the root of the column template is the one that has the offending extra margin. And the second, also assumes that a negative margin will work without clipping; it happens to here, but there are situations in which it wouldn’t.

I think the proper way for this to be fixed would be for the GridViewRowPresenter to pick up the margin from the containing ListViewItem object’s Padding property. This would be consistent with the fact that the vertical and horizontal alignment for row items also come from the ListViewItem. That Padding property doesn’t seem to be used for anything in the ListView right now, but if it were used for this purpose, it would become possible to adjust this with nothing more than a style. That would be much better than writing a hacky shim element. But that’s something only Microsoft can fix.

Copyright © 2002-2024, Interact Software Ltd. Content by Ian Griffiths. Please direct all Web site inquiries to webmaster@interact-sw.co.uk