5

So, it looks really silly to have a chrome-less collection if the items are getting cut/cropped at the end of the scroll region.

I want to create a virtualizing panel for collections (ItemsControl/ListBox) that only draws whole items, never pieces of items. For example:

 ______________
|              |
|______________|
 ______________
|              |
|______________|
 ______________
|              |

enter image description here

I don't want the 3rd partial container to be displayed unless there is room for the WHOLE item/container to be displayed. In the examples, the third item was cropped because of a lack of space.

Any suggestions? Should I try to reinvent the wheel (build my own VirtualizingWholeItemPanel)?

EDIT:

Microsoft clarified that VirtualizingPanel.ScrollUnit is not intended to perform this functionality at all. It appears that VirtualizingPanel.ScrollUnit serves a very similar purpose to the old CanContentScroll on ScrollViewer.

Josh G
  • 14,068
  • 7
  • 62
  • 74
  • Are you changing what `VirtualizingPanel.ScrollUnit` is set to? Because, by default it is set to `Item`, which is what you're looking for. Perhaps post some code and screenshots, and since this is a Developer Preview you could submit a trouble ticket to Microsoft Connect if it is truly a bug? – myermian Feb 23 '12 at 15:36
  • I'm explicitly setting `ScrollUnit` to `Item`. I'll edit and add my sample code. – Josh G Feb 23 '12 at 15:45
  • From what I know that is a "feature" of virtualization. For speed it does not reevaluate width when the display containers are reused. You could try horizontalcontent alignment = stretch. Or you could iterate the string list and explictily set the width to accommodate the largest. Or you could set a fixed width you think will handle most and turn on text wrapping. – paparazzo Feb 23 '12 at 16:01
  • I not worried about the width as much as only displaying whole / complete containers. These items are not getting cropped because the host is not wide enough. They are getting cropped by the `ScrollViewer`. – Josh G Feb 23 '12 at 16:03
  • They are getting cropped because there is not enough room for both. Granted this my not be the effect you want but give it a try. Put an explicit width on both the ListBox and TextBox. Make the ListBox 10 bigger than the TextBox. You can also use a converter to make the TextBox 10 smaller than ListBox. – paparazzo Feb 23 '12 at 16:28
  • Try removing both the CacheLength attached properties... perhaps they are interfering? – myermian Feb 23 '12 at 16:28
  • If you set the verticalscrollbarvisibility to visible then it will probably make room for the first set but possibly not account for bigger content later. I get this a lot with GridView and I use a converter to set the width of one column from the GridView actual width - the width of the other columns - 10 for the scrollbar. I put up with it because GridView is fast. – paparazzo Feb 23 '12 at 16:46
  • I did all of the following: Set ListBox width to 280 and height to 180. Removed the CacheLength and CacheLengthUnit properties. Added ScrollViewer.VerticalScrollBarVisibility="Visible". Set the Grid in the DataTemplate to Width="180". No dice, same result. This problem has nothing to do with the width. I really don't want to show the scrollbar if scrolling is not required. I created a bug report on MicrosoftConnect. – Josh G Feb 23 '12 at 16:47
  • My comment said TextBlock not Grid. Try setting the TextBlock width to 180 (not the Template or the Grid in the Template). Is it not the TextBlock that is getting cropped? – paparazzo Feb 23 '12 at 17:05
  • No, it is the ContentPresenter. – Josh G Feb 23 '12 at 17:12
  • I see. I tried text only and turned off virtualization and it still cropped. I thought I had fixed that once. Sorry I was of basically no help. – paparazzo Feb 23 '12 at 19:24
  • 1
    Perhaps you should update your question with the new information Microsoft has provided you in your [Microsoft Connect bug report](https://connect.microsoft.com/VisualStudio/feedback/details/726364/virtualizingpanel-scrollunit-does-not-appear-to-work-as-intended) :) – BoltClock Mar 08 '12 at 01:46

1 Answers1

4

I have a helper method which I use to determine if a control is partially or completly visible within a parent container. You can probably use it with a Converter to determine the items' visibility.

Your converter would either need to calculate the parent container from the UI item (My blog has a set of Visual Tree Helpers that could assist with this if you want), or it could be a MultiConverter that accepts both the UI item and the parent container as parameters.

ControlVisibility ctrlVisibility= 
    WPFHelpers.IsObjectVisibleInContainer(childControl, parentContainer);

if (ctrlVisibility == ControlVisibility.Full 
    || isVisible == ControlVisibility.FullHeightPartialWidth)
{
    return Visibility.Visible;
}
else
{
    return = Visibility.Hidden;
}

The code to determine a control's visibility within it's parent looks like this:

public enum ControlVisibility
{
    Hidden,
    Partial,
    Full,
    FullHeightPartialWidth,
    FullWidthPartialHeight
}


/// <summary>
/// Checks to see if an object is rendered visible within a parent container
/// </summary>
/// <param name="child">UI element of child object</param>
/// <param name="parent">UI Element of parent object</param>
/// <returns>ControlVisibility Enum</returns>
public static ControlVisibility IsObjectVisibleInContainer(
    FrameworkElement child, UIElement parent)
{
    GeneralTransform childTransform = child.TransformToAncestor(parent);
    Rect childSize = childTransform.TransformBounds(
        new Rect(new Point(0, 0), new Point(child.ActualWidth, child.ActualHeight)));

    Rect result = Rect.Intersect(
        new Rect(new Point(0, 0), parent.RenderSize), childSize);

    if (result == Rect.Empty)
    {
        return ControlVisibility.Hidden;
    }
    if (Math.Round(result.Height, 2) == childSize.Height 
        && Math.Round(result.Width, 2) == childSize.Width)
    {
        return ControlVisibility.Full;
    }
    if (result.Height == childSize.Height)
    {
        return ControlVisibility.FullHeightPartialWidth;
    }
    if (result.Width == childSize.Width)
    {
        return ControlVisibility.FullWidthPartialHeight;
    }
    return ControlVisibility.Partial;
}

Edit

Did some tests and apparently the converter gets run before controls are actually rendered. As a hack, it will work if you use a MultiConverter and pass it the ActualHeight of the control, which will force the converter to re-evaluate when the control gets rendered.

Here's the converter I was using:

public class TestConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        FrameworkElement child = values[0] as FrameworkElement;
        var parent = VisualTreeHelpers.FindAncestor<ListBox>(child);

        ControlVisibility ctrlVisibility =
            VisualTreeHelpers.IsObjectVisibleInContainer(child, parent);

        if (ctrlVisibility == ControlVisibility.Full
            || ctrlVisibility == ControlVisibility.FullHeightPartialWidth)
        {
            return Visibility.Visible;
        }
        else
        {
            return Visibility.Hidden;
        }
    }

    public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
    {
        return null;
    }
}

I used the XAML you posted in your question, and just added an implicit style for ListBoxItem in the .Resources

<ListBox.Resources>
    <Style TargetType="{x:Type ListBoxItem}">
        <Setter Property="Visibility">
            <Setter.Value>
                <MultiBinding Converter="{StaticResource Converter}">
                    <Binding RelativeSource="{RelativeSource Self}" />
                    <Binding RelativeSource="{RelativeSource Self}" Path="ActualHeight" />
                </MultiBinding>
            </Setter.Value>
        </Setter>
    </Style>
</ListBox.Resources>
Rachel
  • 130,264
  • 66
  • 304
  • 490
  • ControlVisibility could be turned into a `[Flags]` enumeration. Nothing game changing, but just a suggestion. – myermian Feb 23 '12 at 16:31
  • 1
    @m-y: I don't think there would be any reason to make it a flags enumeration. The value should never be equal to more than one of the possibilities. From my understanding flags enums are used for concatenating multiple values together with boolean &, |. – Josh G Feb 23 '12 at 16:39
  • Well, `Full`, `FullHeightPartialWidth`, and `FullWidthPartialHeight` seem to be values that are related. – myermian Feb 23 '12 at 16:42
  • @m-y The original purpose of the script was to replace lists of buttons with drop-down menus when they exceeded the allowed width, and some templates would allow partially visible items while others wouldn't. I felt that an enum was more descriptive than flags, and Josh is right that a control will never return more than one `ControlVisibility` value – Rachel Feb 23 '12 at 16:58
  • One half dozen or another, it wasn't a demand. It was merely a suggestion to coincide with how .NET code behaves: For example, look at `CompareOptions` where it is marked `Flags` even though there is a possiblity for some combinations to not make sense. It'll work either way though, just like passing in 5 instead of (2 + 3) is the same. – myermian Feb 23 '12 at 18:11
  • So I tried this in my scenario, but it doesn't seem to handle flexible layouts very well. The child width and height are not set, so the IsObjectVisibleInContainer function did not work properly. I changed it to ActualHeight/Width and then DesiredSize and RenderSize, but none of these were initialized. It seems that the binding is getting initialized before the measure / arrange passes and it is not getting refreshed afterwards... – Josh G Feb 23 '12 at 18:59
  • @JoshG Strange, `DataBind` is processed after `Render`, so `ActualHeight`/`ActualWidth` should have a value. In the past I've always used it in the `Loaded` and `SizeChanged` event of objects, but now I'm curious if it will work in a converter. – Rachel Feb 23 '12 at 19:05
  • @JoshG I've updated my answer with a converter that works. You're right that the converter get evaluated before the object gets rendered, so to force it to re-evaluate I switched to using a `MultiConverter` with the `ActualHeight` as one of the bound values. I also switched the `IsObjectVisibleInContainer` to use the `ActualHeight`/`ActualWidth` instead of `Height`/`Width` – Rachel Feb 23 '12 at 19:51