VirtualizingPanel stops ScrollViewer working within Viewbox - wpf

I have a simple custom panel that is hosted within the ItemsPanel of an ItemsControl. The Template of the ItemsControl is updated to surround the ItemsPresenter with a Viewbox and a ScrollViewer. Here is the XAML code:
<ItemsControl ItemsSource="{Binding Buttons, RelativeSource={RelativeSource
AncestorType={x:Type Local:MainWindow}}}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.CanContentScroll="True">
<ItemsControl.Template>
<ControlTemplate TargetType="{x:Type ItemsControl}">
<ScrollViewer CanContentScroll="True">
<Viewbox Stretch="UniformToFill">
<ItemsPresenter />
</Viewbox>
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Local:TestPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Height="250" Width="250" HorizontalAlignment="Center"
Content="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
And the Panel:
public class TestPanel : Panel
{
protected override Size MeasureOverride(Size availableSize)
{
var desiredSize = new Size();
var layoutSlotSize = availableSize;
layoutSlotSize.Height = double.PositiveInfinity;
for (int i = 0, count = InternalChildren.Count; i < count; ++i)
{
UIElement child = InternalChildren[i];
if (child == null) continue;
child.Measure(layoutSlotSize);
var childDesiredSize = child.DesiredSize;
desiredSize.Width = Math.Max(desiredSize.Width, childDesiredSize.Width);
desiredSize.Height += childDesiredSize.Height;
}
return desiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
double verticalOffset = 0;
for (int i = 0, count = InternalChildren.Count; i < count; ++i)
{
UIElement child = InternalChildren[i];
if (child == null) continue;
child.Arrange(new Rect(0, verticalOffset, child.DesiredSize.Width,
child.DesiredSize.Height));
verticalOffset += child.DesiredSize.Height;
}
return base.ArrangeOverride(finalSize);
}
}
And finally, MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Buttons = new ObservableCollection<string>();
IEnumerable<int> characterCodes = Enumerable.Range(65, 26);
foreach (int characterCode in characterCodes)
Buttons.Add(((char)characterCode).ToString().ToUpper());
}
public static readonly DependencyProperty ButtonsProperty =
DependencyProperty.Register(nameof(Buttons), typeof(ObservableCollection<string>),
typeof(MainWindow), null);
public ObservableCollection<string> Buttons
{
get { return (ObservableCollection<string>)GetValue(ButtonsProperty); }
set { SetValue(ButtonsProperty, value); }
}
}
This all works as expected... so far, so good. The problem is when I change the base class from Panel to VirtualizingPanel, which I need to do to virtualise the data (not this example button data). After changing the base class, the panel immediately stops working. I am totally aware of how to virtualize data in a panel... I have a working example of this. My problem is when I want to put add a Viewbox inside the ScrollViewer.
Please note that this XAML will work fine with a normal Panel, or StackPanel, but as soon as I change it to VirtualizingPanel, it stops working (nothing is rendered, and the InternalChildren property contains no elements). Can anyone shed some light on this problem for me please?

I still do not know why the VirtualizingPanel does not work within a ViewBox within a ScrollViewer, but I have discovered that if I extend the VirtualizingStackPanel class in my panel instead, everything works as expected.
Therefore, the solution for those who require virtualized items to be stacked is to extend the VirtualizingStackPanel class instead. For those who need other types of child arrangement, I'm sorry, but I have no answer, unless you remove the Viewbox.
I would still be more than happy to receive any further information on this subject.

Related

Set Min and Max height of items in ItemsControl

I am using an ItemsControl to display a list of 1 - 10 items (usually 2 - 4). I am trying to satisfy all these requirements:
All rows must be the same height
All rows should be displayed at a height of 300 maximum if possible.
If there is not enough room to display all rows at 300 high, then display at the largest possible height.
If the largest possible height is less than 150, then display at maxsize and use a scrollbar
If the rows do not fill the page, then it must be vertically aligned at the top
This is what I have so far:
<Window x:Class="TestGridRows.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:TestGridRows"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance vm:MainViewModel}"
Height="570" Width="800">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Path=DataItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border MinHeight="150" MaxHeight="300" BorderBrush="DarkGray" BorderThickness="1" Margin="5">
<TextBlock Text="{Binding Path=TheNameToDisplay}" VerticalAlignment="Center" HorizontalAlignment="Center" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="1" IsItemsHost="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
</Window>
This is what it currently looks like with 1 item:
and this is what it should look like:
2 or 3 items display as expected:
For 4+ items, the scrollbar appears correctly but the items are all sized to 150, rather than 300:
Question
How do I align the content to top when there is only 1 item? (without breaking the other functionality obviously)
Bonus question: How do I get the items to resize to maxheight instead of minheight when there are 4+ items?
During the WPF layout process, measuring and arranging will be done in order. In most cast, if an UIElement has variable size, it will return minimum required as result. But if any layout alignment has been set to Stretch, UIElement will take as possible as it can in that direction in arranging. In your case, UniFormGrid will always return 160(which is Border.MinHeight + Border.Margin.Top + Border.Margin.Bottom) * the count of items as desired height in measuring result(which will stored in DesiredSize.DesiredSize.Height). But it will take ItemsControl.ActualHeight as arranged height since it has Stretch VerticalAlignment. So, if UniFormGrid.DesiredSize.Height was less then ItemsControl.ActualHeight, UniFormGrid and any child has Stretch VerticalAlignment will be stretch in vertically, until it encountered its MaxHeight. This is why your 1 item test resulted in the center. If you change UniFormGrid.VerticalAlignment or Border.VerticalAlignment to Top, you will get a 160 height item in the top of ItemsContorl.
The most simple solution to both questions is override the measuring result base on maximum row height and minimum row height. I write the codes in below and had done some basic tests, it seems to work just fine.
namespace WpfApp1
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
public class MyScrollViewer : ScrollViewer
{
public double DesiredViewportHeight;
public MyScrollViewer() : base() { }
protected override Size MeasureOverride(Size constraint)
{
// record viewport's height for late calculation
DesiredViewportHeight = constraint.Height;
var result = base.MeasureOverride(constraint);
// make sure that `ComputedVerticalScrollBarVisibility` will get correct value
if (ComputedVerticalScrollBarVisibility == Visibility.Visible && ExtentHeight <= ViewportHeight)
result = base.MeasureOverride(constraint);
return result;
}
}
public class MyUniformGrid : UniformGrid
{
private MyScrollViewer hostSV;
private ItemsControl hostIC;
public MyUniFormGrid() : base() { }
public double MaxRowHeight { get; set; }
public double MinRowHeight { get; set; }
protected override Size MeasureOverride(Size constraint)
{
if (hostSV == null)
{
hostSV = VisualTreeHelperEx.GetAncestor<MyScrollViewer>(this);
hostSV.SizeChanged += (s, e) =>
{
if (e.HeightChanged)
{
// need to redo layout pass after the height of host had changed.
this.InvalidateMeasure();
}
};
}
if (hostIC == null)
hostIC = VisualTreeHelperEx.GetAncestor<ItemsControl>(this);
var viewportHeight = hostSV.DesiredViewportHeight;
var rows = hostIC.Items.Count;
var rowHeight = viewportHeight / rows;
double desiredHeight = 0;
// calculate the correct height
if (rowHeight > MaxRowHeight || rowHeight < MinRowHeight)
desiredHeight = MaxRowHeight * rows;
else
desiredHeight = viewportHeight;
var result = base.MeasureOverride(constraint);
return new Size(result.Width, desiredHeight);
}
}
public class VisualTreeHelperEx
{
public static T GetAncestor<T>(DependencyObject reference, int level = 1) where T : DependencyObject
{
if (level < 1)
throw new ArgumentOutOfRangeException(nameof(level));
return GetAncestorInternal<T>(reference, level);
}
private static T GetAncestorInternal<T>(DependencyObject reference, int level) where T : DependencyObject
{
var parent = VisualTreeHelper.GetParent(reference);
if (parent == null)
return null;
if (parent is T && --level == 0)
return (T)parent;
return GetAncestorInternal<T>(parent, level);
}
}
}
Xaml
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
Height="570" Width="800">
<local:MyScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl>
<sys:String>aaa</sys:String>
<sys:String>aaa</sys:String>
<sys:String>aaa</sys:String>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="DarkGray" BorderThickness="1" Margin="5">
<TextBlock Text="{Binding ActualHeight, RelativeSource={RelativeSource AncestorType=ContentPresenter}}"
VerticalAlignment="Center" HorizontalAlignment="Center" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<local:MyUniformGrid Columns="1" MinRowHeight="150" MaxRowHeight="300" VerticalAlignment="Top"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</local:MyScrollViewer>
</Window>

WPF, interesting things when binding with TemplatedParent

Control template:
<ControlTemplate x:Key="BasicShape2">
<StackPanel Name="sp">
<Border Name="bd" CornerRadius="3.5" BorderThickness="1" BorderBrush="{Binding RelativeSource={RelativeSource TemplatedParent},Path=DataContext.NodeType, Converter={StaticResource NodeTypeColorConverter}, Mode=OneWay}" Height="32" Padding="1">
<TextBlock Name="tbName" Grid.Column="1" Text="" HorizontalAlignment="Center" VerticalAlignment="Bottom" FontSize="16" />
</Border>
</StackPanel>
</ControlTemplate>
a class which this template will apply to:
public class MyThumbEx : Thumb
{
public static readonly DependencyProperty MemberInfoProperty = DependencyProperty.Register("MemberInfo", typeof(FamilyMemberInfo), typeof(MyThumbEx));
public FamilyMemberInfo MemberInfo
{
get { return (FamilyMemberInfo)GetValue(MemberInfoProperty); }
set { SetValue(MemberInfoProperty, value); }
}
public MyThumbEx(ControlTemplate template, FamilyMemberInfo info, Point position)
{
this.MemberInfo = info;
this.DataContext = this.MemberInfo;
this.Template = template;
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.ApplyTextContent();
}
public void ApplyTextContent()
{
TextBlock tbName = this.Template.FindName("tbName", this) as TextBlock;
if (tbName != null)
{
tbName.Text = this.MemberInfo.Name;
}
}
}
initialize and display it on a canvas:
public MainWindow()
{
InitializeComponent();
//
FamilyMemberInfo mi = new FamilyMemberInfo();
mi.Name = "someone";
mi.ID = "id1";
MyThumbEx te = new MyThumbEx(Application.Current.Resources["BasicShape2"] as ControlTemplate, mi, new Point(0, 0));
//
this.cvMain.Children.Add(te);
}
These codes work fine, but be noticed that in the control template, I have to set Path=DataContext.NodeType, not just Path=NodeType. I'm new to WPF, and I found that normally, when I did binding without using this template stuff, I didn't need to specify the predicate 'DataContext', right? Why we need here?
Another thing I found is, I can comment out this.DataContext = this.MemberInfo, and change binding path to Path=MemberInfo.NodeType, the code still works fine. Could anyone explain that for me?
Thanks in advance!
If you dont change the DataContext manuelly, every child automatically has the DataContext of its Parent. So if your Window has f.e. the ViewModel as DataContext all of its Controls have access to the ViewModels Properties through {Binding Path=Property}.
But in case of a ControlTemplate the usual typical flow where DataContext just cascades through from the parent to child doesn’t apply here. So you have to set the DataContext first, either through Property="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=DataContext.Property}" or DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=DataContext}" Property="{Binding Path=Property}".
To your second point: It could be, that the ControlTemplate automatically uses the code-behind of its containing Element as DataContext, so you can use the code-behinds properties without setting the DataContext, but I am not 100% sure about this.

How do I bind to the preferred size of a ListView?

I have a WPF ListView that should be extended with an always visible footer.
The footer shall behave like a header and should not be scrolled away.
The following XAML uses an external ScrollViewer linked to code behind to steer the ScrollViewer of the ListView:
<Window x:Class="LayoutTests.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="125" Width="176">
<Grid>
<StackPanel>
<ListView Name="L" ScrollViewer.HorizontalScrollBarVisibility="Hidden">
<ListViewItem Content="Brown brownie with a preference for white wheat."/>
<ListViewItem Content="Red Redish with a taste for oliv olives."/>
</ListView>
<ScrollViewer CanContentScroll="False" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" ScrollChanged="ScrollViewer_ScrollChanged">
<!-- Would like to bind Rectangle.Width to the preferred width of L -->
<Rectangle Height="20" Width="500" Fill="Red"/>
</ScrollViewer>
</StackPanel>
</Grid>
</Window>
In the code behind this looks like this:
private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
var bottomScrollViewer = sender as ScrollViewer;
var listScrollViewer = GetScrollViewer(L) as ScrollViewer;
if (listScrollViewer != null && bottomScrollViewer != null )
listScrollViewer.ScrollToHorizontalOffset( bottomScrollViewer.HorizontalOffset );
}
GetScrollViewer() is defined like this (but unimportant):
public static DependencyObject GetScrollViewer(DependencyObject depObj)
{
if (depObj is ScrollViewer)
{ return depObj; }
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = GetScrollViewer(child);
if (result == null) { continue; }
else { return result; }
}
return null;
}
The ScrollViewer of ListView obviously knows about the preferred width of its children.
The problem is that I cant find a way to bind to that width. So here it is:
How do I bind Rectangle.Width to the preferred size of the ListView?
Or, alternatively, how do I include a footer in the ListView that is always visible?
You need to bind against ExtentWidth of your ScrollViewer. According to http://msdn.microsoft.com/en-us/library/system.windows.controls.scrollviewer.extentwidth.aspx, it's a DependencyProperty. Mind that you need the ScrollViewer of your ListView, not the additional one you are creating below the list view.
You can use your GetScrollViewer function to find the ScrollViewer on the ListView. Of course, you'll need to set the binding in the code-behind. Something like that:
Binding b = new Binding("ExtentWidth") { Source = GetScrollViewer(L) };
rect.SetBinding(Rectangle.WidthProperty, b);

Nested Scroll Areas

I creating a control for WPF, and I have a question for you WPF gurus out there.
I want my control to be able to expand to fit a resizable window.
In my control, I have a list box that I want to expand with the window. I also have other controls around the list box (buttons, text, etc).
I want to be able to set a minimum size on my control, but I want the window to be able to be sized smaller by creating scroll bars for viewing the control.
This creates nested scroll areas: One for the list box and a ScrollViewer wrapping the whole control.
Now, if the list box is set to auto size, it will never have a scroll bar because it is always drawn full size within the ScrollViewer.
I only want the control to scroll if the content can't get any smaller, otherwise I don't want to scroll the control; instead I want to scroll the list box inside the control.
How can I alter the default behavior of the ScrollViewer class? I tried inheriting from the ScrollViewer class and overriding the MeasureOverride and ArrangeOverride classes, but I couldn't figure out how to measure and arrange the child properly. It appears that the arrange has to affect the ScrollContentPresenter somehow, not the actual content child.
Any help/suggestions would be much appreciated.
I've created a class to work around this problem:
public class RestrictDesiredSize : Decorator
{
Size lastArrangeSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
protected override Size MeasureOverride(Size constraint)
{
Debug.WriteLine("Measure: " + constraint);
base.MeasureOverride(new Size(Math.Min(lastArrangeSize.Width, constraint.Width),
Math.Min(lastArrangeSize.Height, constraint.Height)));
return new Size(0, 0);
}
protected override Size ArrangeOverride(Size arrangeSize)
{
Debug.WriteLine("Arrange: " + arrangeSize);
if (lastArrangeSize != arrangeSize) {
lastArrangeSize = arrangeSize;
base.MeasureOverride(arrangeSize);
}
return base.ArrangeOverride(arrangeSize);
}
}
It will always return a desired size of (0,0), even if the containing element wants to be bigger.
Usage:
<local:RestrictDesiredSize MinWidth="200" MinHeight="200">
<ListBox />
</local>
You problem arises, because Controls within a ScrollViewer have virtually unlimited space available. Therefore your inner ListBox thinks it can avoid scrolling by taking up the complete height necessary to display all its elements. Of course in your case that behaviour has the unwanted side effect of exercising the outer ScrollViewer too much.
The objective therefore is to get the ListBox to use the visible height within the ScrollViewer iff there is enough of it and a certain minimal height otherwise. To achieve this, the most direct way is to inherit from ScrollViewer and override MeasureOverride() to pass an appropriately sized availableSize (that is the given availableSize blown up to the minimal size instead of the "usual" infinity) to the Visuals found by using VisualChildrenCount and GetVisualChild(int).
I used Daniels solution. That works great. Thank you.
Then I added two boolean dependency properties to the decorator class: KeepWidth and KeepHeight. So the new feature can be suppressed for one dimension.
This requires a change in MeasureOverride:
protected override Size MeasureOverride(Size constraint)
{
var innerWidth = Math.Min(this._lastArrangeSize.Width, constraint.Width);
var innerHeight = Math.Min(this._lastArrangeSize.Height, constraint.Height);
base.MeasureOverride(new Size(innerWidth, innerHeight));
var outerWidth = KeepWidth ? Child.DesiredSize.Width : 0;
var outerHeight = KeepHeight ? Child.DesiredSize.Height : 0;
return new Size(outerWidth, outerHeight);
}
While I wouldn't recommend creating a UI that requires outer scroll bars you can accomplish this pretty easily:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ListBox Grid.Row="0" Grid.RowSpan="3" Grid.Column="0" MinWidth="200"/>
<Button Grid.Row="0" Grid.Column="1" Content="Button1"/>
<Button Grid.Row="1" Grid.Column="1" Content="Button2"/>
<Button Grid.Row="2" Grid.Column="1" Content="Button3"/>
</Grid>
</ScrollViewer>
</Window>
I don't really recommend this. WPF provides exceptional layout systems, like Grid, and you should try to allow the app to resize itself as needed. Perhaps you can set a MinWidth/MinHeight on the window itself to prevent this resizing?
Create a method in the code-behind that sets the ListBox's MaxHeight to the height of whatever control is containing it and other controls. If the Listbox has any controls/margins/padding above or below it, subtract their heights from the container height assigned to MaxHeight. Call this method in the main windows "loaded" and "window resize" event handlers.
This should give you the best of both worlds. You are giving the ListBox a "fixed" size that will cause it to scroll in spite of the fact that the main window has its own scrollbar.
for 2 ScrollViewer
public class ScrollExt: ScrollViewer
{
Size lastArrangeSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
public ScrollExt()
{
}
protected override Size MeasureOverride(Size constraint)
{
base.MeasureOverride(new Size(Math.Min(lastArrangeSize.Width, constraint.Width),
Math.Min(lastArrangeSize.Height, constraint.Height)));
return new Size(0, 0);
}
protected override Size ArrangeOverride(Size arrangeSize)
{
if (lastArrangeSize != arrangeSize)
{
lastArrangeSize = arrangeSize;
base.MeasureOverride(arrangeSize);
}
return base.ArrangeOverride(arrangeSize);
}
}
code:
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Background="Beige" Width="600" Text="Example"/>
<Grid Grid.Column="1" x:Name="grid">
<Grid Grid.Column="1" Margin="25" Background="Green">
<local:ScrollExt HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Grid Width="10000" Margin="25" Background="Red" />
</local:ScrollExt>
</Grid>
</Grid>
</Grid>
</ScrollViewer>
I ended up combining Daniels answer and Heiner's answer. I decided to post the entire solution to make it easier for people to adopt this if needed. Here's my decorator class:
public class RestrictDesiredSizeDecorator : Decorator
{
public static readonly DependencyProperty KeepWidth;
public static readonly DependencyProperty KeepHeight;
#region Dependency property setters and getters
public static void SetKeepWidth(UIElement element, bool value)
{
element.SetValue(KeepWidth, value);
}
public static bool GetKeepWidth(UIElement element)
{
return (bool)element.GetValue(KeepWidth);
}
public static void SetKeepHeight(UIElement element, bool value)
{
element.SetValue(KeepHeight, value);
}
public static bool GetKeepHeight(UIElement element)
{
return (bool)element.GetValue(KeepHeight);
}
#endregion
private Size _lastArrangeSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
static RestrictDesiredSizeDecorator()
{
KeepWidth = DependencyProperty.RegisterAttached(
nameof(KeepWidth),
typeof(bool),
typeof(RestrictDesiredSizeDecorator));
KeepHeight = DependencyProperty.RegisterAttached(
nameof(KeepHeight),
typeof(bool),
typeof(RestrictDesiredSizeDecorator));
}
protected override Size MeasureOverride(Size constraint)
{
Debug.WriteLine("Measure: " + constraint);
var keepWidth = GetValue(KeepWidth) as bool? ?? false;
var keepHeight = GetValue(KeepHeight) as bool? ?? false;
var innerWidth = keepWidth ? constraint.Width : Math.Min(this._lastArrangeSize.Width, constraint.Width);
var innerHeight = keepHeight ? constraint.Height : Math.Min(this._lastArrangeSize.Height, constraint.Height);
base.MeasureOverride(new Size(innerWidth, innerHeight));
var outerWidth = keepWidth ? Child.DesiredSize.Width : 0;
var outerHeight = keepHeight ? Child.DesiredSize.Height : 0;
return new Size(outerWidth, outerHeight);
}
protected override Size ArrangeOverride(Size arrangeSize)
{
Debug.WriteLine("Arrange: " + arrangeSize);
if (_lastArrangeSize != arrangeSize)
{
_lastArrangeSize = arrangeSize;
base.MeasureOverride(arrangeSize);
}
return base.ArrangeOverride(arrangeSize);
}
}
and here's how I use it in the xaml:
<ScrollViewer>
<StackPanel Orientation="Vertical">
<Whatever />
<decorators:RestrictDesiredSizeDecorator MinWidth="100" KeepHeight="True">
<TextBox
Text="{Binding Comment, UpdateSourceTrigger=PropertyChanged}"
Height="Auto"
MaxHeight="360"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
AcceptsReturn="True"
AcceptsTab="True"
TextWrapping="WrapWithOverflow"
/>
</decorators:RestrictDesiredSizeDecorator>
<Whatever />
</StackPanel>
</ScrollViewer
The above creates a textbox that will grow vertically (until it hits MaxHeight) but will match the parent's width without growing the outer ScrollViewer. Resizing the window/ScrollViewer to less than 100 wide will force the outer ScrollViewer to show the horizontal scroll bars. Other controls with inner ScrollViewers can be used as well, including complex grids.

Outside Property inside a DataTemplate WPF

Scenario: I have a ListBox and the ListBoxItems have a DataTemplate. What I want to do is put a ContextMenu in the DataTemplate. The catch is that I want this ContextMenu ItemsSource to be different depending on certain properties in the window. My initial thought is that I could just bind the ItemsSource to a Property in the window and that would return an ItemsSource; however, I cant seem to bind to this property correctly. I believe this is because I am in the DataTemplate and consequently the DataContext (I believe that is the right word) is of that ListBoxItem and not of the window.
How could I get the ContextMenu that is inside a DataTemplate to bind to a Property outside of the DataTemplate.
You can get the DataContext from your window by using the RelativeSource FindAncestor syntax
<DataTemplate>
<TextBlock Text="{Binding MyInfo}">
<TextBlock.ContextMenu>
<Menu ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.MyContextMenuItems}"/>
</TextBlock.ContextMenu>
</TextBlock>
</DataTemplate>
Not totally sure, but the binding is correct...
If your DataContext is on another object type, you just have to change the AncestorType (eg. by UserControl).
This might be a good candidate for an AttachedProperty. Basically what you would do is wrap your ContextMenu in a UserControl and then add a Dependency Property to the UserControl. For example:
MyContextMenu.xaml
<UserControl x:Class="MyContextMenu" ...>
<UserControl.Template>
<ContextMenu ItemSource="{Binding}" />
</UserControl.Template>
</UserControl>
MyContextMenu.xaml.cs
public static readonly DependencyProperty MenuItemsSourceProperty = DependencyProperty.RegisterAttached(
"MenuItemsSource",
typeof(Object),
typeof(MyContextMenu),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender)
);
public static void SetMenuItemsSource(UIElement element, Boolean value)
{
element.SetValue(MenuItemsSourceProperty, value);
// assuming you want to change the context menu when the mouse is over an element.
// use can use other events. ie right mouse button down if its a right click menu.
// you may see a perf hit as your changing the datacontext on every mousenter.
element.MouseEnter += (s, e) => {
// find your ContextMenu and set the DataContext to value
var window = element.GetRoot();
var menu = window.GetVisuals().OfType<MyContextMenu>().FirstOrDefault();
if (menu != null)
menu.DataContext = value;
}
}
public static Object GetMenuItemsSource(UIElement element)
{
return element.GetValue(MenuItemsSourceProperty);
}
Window1.xaml
<Window ...>
<Window.Resources>
<DataTemplate TargetType="ListViewItem">
<Border MyContextMenu.MenuItemsSource="{Binding Orders}">
<!-- Others -->
<Border>
</DataTemplate>
</Window.Resources>
<local:MyContextMenu />
<Button MyContextMenu.MenuItemsSource="{StaticResource buttonItems}" />
<ListView ... />
</Window>
VisualTreeHelpers
public static IEnumerable<DependencyObject> GetVisuals(this DependencyObject root)
{
int count = VisualTreeHelper.GetChildrenCount(root);
for (int i = 0; i < count; i++)
{
var child = VisualTreeHelper.GetChild(root, i);
yield return child;
foreach (var descendants in child.GetVisuals())
{
yield return descendants;
}
}
}
public static DependencyObject GetRoot(this DependencyObject child)
{
var parent = VisualTreeHelper.GetParent(child)
if (parent == null)
return child;
return parent.GetRoot();
}
This example is un-tested I'll take a look later tonight and make sure its accurate.

Resources