none
Binding a scrollviewer height

    Question

  • i'm using a stack of expanders to mimick an accordion control. I have an ItemsControl on the page with it's ItemsSource set to Screens. Here is some sample code for the ViewModel.

    	internal class MainWindowViewModel
    {
    ObservableCollection<Expander> _screens = new ObservableCollection<Expander>();
    public ObservableCollection<Expander> Screens { get { return _screens; } }

    internal Dictionary<stringUserControl> AppScreens = new Dictionary<stringUserControl>() 

    {"first"new screen()},
    {"second"new screen()},
    {"third"new screen()}
    };

    public MainWindowViewModel()
    {
    if (AppScreens == nullreturn;
    foreach (var item in AppScreens)
    {
    ScrollViewer _scroll = new ScrollViewer();
    _scroll.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
    _scroll.Content = item.Value;

    Border _brdr = new Border();
    _brdr.Style = _brdr.FindResource("ScreenStyle"as Style;
    _brdr.Height = 300; //this gets the result I need, but I don't want to hard code it, I want it to use the available height
    _brdr.Child = _scroll;

    Expander _scn = new Expander();
    _scn.Header = item.Key;
    _scn.Content = _brdr;
    _screens.Add(_scn);
    }
    }
    }

    The problem is that without any height specified on the Border, it simply grows to the size of the content and the ScrollViewer is useless. I just end up with the UI getting truncated if it doesn't fit. I've been looking at some way of using the ScreenStyle resource to specify the height but haven't been successful. I can wrap a scrollviewer around the entire ItemsControl, but that causes all of the items/screens to scroll off. I want JUST the content of the expander being displayed to have the scrollbars. I've basically looked at over engineering this a number of ways (e.g. IValueConverter) which haven't worked so I want to take a step back and get some other eyes on it. I'm hoping there is something simple (e.g. a different container control) that will make this a snap?

    I put some sample code here if you want to tinker with it.

    http://cid-34739e178c9c1fa3.skydrive.live.com/self.aspx/Code/WpfApplication1.zip

    Thanks in advance!

     Drew

     

     

     

     

     

     

    Saturday, October 09, 2010 8:54 PM

Answers

  • alrighty then... i think i finally stumbled on to the solution. My stack of Expanders use templates to highly customize the visuals and behavior. In the template I setup a Trigger to set the height of my content row in the grid that represents the new layout. I used a custom IMultiValueConverter to calculate the height.

    here is the trigger

    					<ControlTemplate.Triggers>
    <Trigger Property="IsExpanded" Value="True">
    <Setter TargetName="ContentRow" Property="Height">
    <Setter.Value>
    <MultiBinding Converter="{StaticResource HeightConverter}">
    <Binding Path="Items.Count" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}" />
    <Binding Path="ActualHeight" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}" />
    <Binding Path="Height.Value" ElementName="HeaderRow" />
    </MultiBinding>
    </Setter.Value>
    </Setter> 
    </Trigger>
    </ControlTemplate.Triggers>

    And here is the converter

    	internal class FillHeightConverter : IMultiValueConverter
    {
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
    try
    {
    if (values.Length < 3) return 100;
    int _screenCount = (int)values[0];
    double _containerHeight = (double)values[1];
    double _headerHeight = (double)values[2];
    GridLengthConverter _conv = new GridLengthConverter();
    return _conv.ConvertFrom(_containerHeight - (_screenCount * _headerHeight));
    }
    catch { return 100; }
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
    throw new NotImplementedException();
    }
    }
    so far, so good...
    Thursday, October 21, 2010 5:12 AM

All replies

  • Hi DigitalD,

    It is hard to the get the remained area size of the ItemsControl, there is no property that can calculate this area size. So it is impossible to use DataBinding with the Expander and the ItemsControl.

    How about to implement a custom ItemsControl that can make the Item fit to the content panel? I coded a sample, a custom ItemsControl to resize the Expander size based on the content:

     public class myItemsControl : ItemsControl
     {
     protected override Size MeasureOverride(Size constraint)
     {
      ItemsControl itemscontrol = this as ItemsControl;
      int expanedCount = 0;
      double collapsedHeight = 23; // 23 is the default height of the Expander
      for (int i = 0; i < itemscontrol.Items.Count; i++)
      {
      Expander expander = itemscontrol.ItemContainerGenerator.ContainerFromIndex(i) as Expander;
      if (expander.IsExpanded) expanedCount++;
      }
      for (int i = 0; i < itemscontrol.Items.Count; i++)
      {
      Expander expander = itemscontrol.ItemContainerGenerator.ContainerFromIndex(i) as Expander;
      if (expanedCount != 0 && expander.IsExpanded)
       expander.Height = (constraint.Height - collapsedHeight * (itemscontrol.Items.Count - expanedCount)) / expanedCount;
      else
       expander.Height = collapsedHeight;
      }
      return base.MeasureOverride(constraint);
     }
    

    and some changes for the expander, to invoke the parent (myItemsControl) InvalidateMeasure() when it is expanding or collapsing:

      ......
      Border _brdr = new Border();
      _brdr.Style = _brdr.FindResource("ScreenStyle") as Style;
      _brdr.Child = _scroll;
      Expander _scn = new Expander();
      _scn.Header = item.Key;
      _scn.Content = _brdr;
      _scn.Collapsed += (o, e) =>
      {
       var par = FindVisualParent<myItemsControl>((o as Expander) as DependencyObject);
       par.InvalidateMeasure();
      };
      _scn.Expanded += (o, e) =>
      {
       var par = FindVisualParent<myItemsControl>((o as Expander) as DependencyObject);
       par.InvalidateMeasure();
      };
      _screens.Add(_scn);
      }
     }
    
     public T FindVisualParent<T>(DependencyObject child) where T : DependencyObject
     {
      DependencyObject parentObject = VisualTreeHelper.GetParent(child);
      if (parentObject == null) return null;
      T parent = parentObject as T;
      if (parent != null)
      return parent;
      else
      return FindVisualParent<T>(parentObject);
     }
    

    Hope this helps.

    Sincerely,
    Bob Bao

    MSDN Subscriber Support in Forum 

    If you have any feedback on our support, please contact msdnmg@microsoft.com


    Please remember to mark the replies as answers if they help and unmark them if they provide no help.
    Are you looking for a typical code sample? Please download all in one code framework !
    Monday, October 11, 2010 5:07 AM
  • Well, that's closer than i got! but not what i'm looking for. In this solution if the size shrinks too far, setting the exapander height actually truncates the scrollviewer so you can't even get to the scrollbars. Another note to make this slightly easier, there will only ever be 1 expander expanded.  

    I'm now trying a slightly different approach, which I thought would be easier, it's not. I'm trying to use the ItemsPanel or a Control template with the right containers, but haven't found anything that works. if i use the DockPanel and it docks to the left, it DOES automatically adjust the height to match the available space, but not if it's docked to the top as I want.

    surely an accoridon that fills all the available space should be easier!!

    			<ItemsControl Name="itemsScreens" Grid.Column="1" Grid.Row="1" ItemsSource="{Binding Screens}">
    <ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
    <DockPanel LastChildFill="True" />
    </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemContainerStyle>
    <Style>
    <Setter Property="DockPanel.Dock" Value="Top" />
    </Style>
    </ItemsControl.ItemContainerStyle>
    <!--<ItemsControl.Template>
    <ControlTemplate TargetType="ItemsControl">
    <DockPanel>
    <ItemsPresenter />
    </DockPanel>
    </ControlTemplate>
    </ItemsControl.Template>-->
    <ItemsControl.ItemTemplate>
    <DataTemplate>
    <Border>
    <Expander Header="{Binding Path=Title}">
    <ScrollViewer VerticalScrollBarVisibility="Auto" Content="{Binding Path=Interface}" />
    </Expander>
    </Border>
    </DataTemplate>
    </ItemsControl.ItemTemplate>
    </ItemsControl>
    Tuesday, October 19, 2010 1:28 AM
  • Hi DigitalD,

    Perhaps you need a custom panel to host the items, since the system defined panels do not have this behaviour to match the available space for each child item (like the Grid panel, but it can not generate the Row and Column automatically for ItemsControl). Please refer to the Layout System. and Creating Custom Panels In WPF: http://www.codeproject.com/Articles/37348/Creating-Custom-Panels-In-WPF.aspx

    Sincerely,
    Bob Bao


    Please remember to mark the replies as answers if they help and unmark them if they provide no help.
    Are you looking for a typical code sample? Please download all in one code framework !
    Tuesday, October 19, 2010 9:35 AM
  • alrighty then... i think i finally stumbled on to the solution. My stack of Expanders use templates to highly customize the visuals and behavior. In the template I setup a Trigger to set the height of my content row in the grid that represents the new layout. I used a custom IMultiValueConverter to calculate the height.

    here is the trigger

    					<ControlTemplate.Triggers>
    <Trigger Property="IsExpanded" Value="True">
    <Setter TargetName="ContentRow" Property="Height">
    <Setter.Value>
    <MultiBinding Converter="{StaticResource HeightConverter}">
    <Binding Path="Items.Count" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}" />
    <Binding Path="ActualHeight" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}" />
    <Binding Path="Height.Value" ElementName="HeaderRow" />
    </MultiBinding>
    </Setter.Value>
    </Setter> 
    </Trigger>
    </ControlTemplate.Triggers>

    And here is the converter

    	internal class FillHeightConverter : IMultiValueConverter
    {
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
    try
    {
    if (values.Length < 3) return 100;
    int _screenCount = (int)values[0];
    double _containerHeight = (double)values[1];
    double _headerHeight = (double)values[2];
    GridLengthConverter _conv = new GridLengthConverter();
    return _conv.ConvertFrom(_containerHeight - (_screenCount * _headerHeight));
    }
    catch { return 100; }
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
    throw new NotImplementedException();
    }
    }
    so far, so good...
    Thursday, October 21, 2010 5:12 AM