Nested Scroll Areas - wpf

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.

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>

VirtualizingPanel stops ScrollViewer working within Viewbox

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.

Wpf Scroll Image using ScrollViewer with dynamic Stretch

I working on a simple imageviewer app. I control the Stretch property on the binding based on ViewModel property.
The problem occurs when I change the Stretch attribute based on a 'Combobox', bound to ViewModel, and the image 'cuts off' the corners of a wide image when using 'UniformToFill'. Hence to use of a ScrollViewer to be able to scroll the image content.
The problem is the ScrollViewer doesn't seem to show up scrollbars for me to be able to scroll.
WPF Markup:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Other Grids removed -->
<Grid Name="Container" Grid.Column="2" Grid.Row="0" Grid.RowSpan="2">
<ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible">
<Image Source="{Binding SelectedPhoto.Value.Image}"
Stretch="{Binding ImageStretch}" Name="PhotoImage" />
</ScrollViewer>
</Grid>
I understand if I set a fixed Height and Width to ScrollViewer and Image, it will work. But I want to do it Dynamically:
The ScrollView Will have Height and Width from Parent 'Grid(Contaioner)' Control.
The Image will have Height and Width from itself, but take Stretch to account in that calculation.
Possible to solve with ActualHeight, ActualWidth? And a DependecyProperty?
This is almost impossible, Or I should say it doesn't make a lot of sense to expect ScrollViewer to know the boundaries of an image with Stretch = UniformToFill. According to MSDN:
UniformToFill:
The content (your Image) is resized to fill the destination dimensions (window or grid) while it
preserves its native aspect ratio. If the aspect ratio of the
destination rectangle differs from the source, the source content is
clipped to fit in the destination dimensions (Therefore the image will be cutted off).
So I think what we really need here is to use Uniform + Proper Scaling instead of UniformToFill.
The solution is when Stretch is set to UniformToFill it must set to Uniform and then Image.Width = image actual width * scalingParam and Image.Height= image actual height * scalingParam, where scalingParam = Grid.Width (or Height) / image actual width (or Height). This way ScrollViewer boundaries will be the same as the image scaled size.
I've provided a working solution to give you an Idea, I'm not sure how suitable would it be for your case but here it is:
First I defined a simple view-model for my Images:
public class ImageViewModel: INotifyPropertyChanged
{
// implementation of INotifyPropertyChanged ...
private BitmapFrame _bitmapFrame;
public ImageViewModel(string path, Stretch stretch)
{
// determining the actual size of the image.
_bitmapFrame = BitmapFrame.Create(new Uri(path), BitmapCreateOptions.DelayCreation, BitmapCacheOption.None);
Width = _bitmapFrame.PixelWidth;
Height = _bitmapFrame.PixelHeight;
Scale = 1;
Stretch = stretch;
}
public int Width { get; set; }
public int Height { get; set; }
double _scale;
public double Scale
{
get
{
return _scale;
}
set
{
_scale = value;
OnPropertyChanged("Scale");
}
}
Stretch _stretch;
public Stretch Stretch
{
get
{
return _stretch;
}
set
{
_stretch = value;
OnPropertyChanged("Stretch");
}
}
}
In the above code BitmapFrame is used to determine the actual size of the image.
Then I did some initializations in my Mainwindow (or main view-model):
// currently displaying image
ImageViewModel _imageVm;
public ImageViewModel ImageVM
{
get
{
return _imageVm;
}
set
{
_imageVm = value;
OnPropertyChanged("ImageVM");
}
}
// currently selected stretch type
Stretch _stretch;
public Stretch CurrentStretch
{
get
{
return _stretch;
}
set
{
_stretch = value;
//ImageVM should be notified to refresh UI bindings
ImageVM.Stretch = _stretch;
OnPropertyChanged("ImageVM");
OnPropertyChanged("CurrentStretch");
}
}
// a list of Stretch types
public List<Stretch> StretchList { get; set; }
public string ImagePath { get; set; }
public MainWindow()
{
InitializeComponent();
DataContext = this;
// sample image path
ImagePath = #"C:\Users\...\YourFile.png";
StretchList = new List<Stretch>();
StretchList.Add( Stretch.None);
StretchList.Add( Stretch.Fill);
StretchList.Add( Stretch.Uniform);
StretchList.Add( Stretch.UniformToFill);
ImageVM = new ImageViewModel(ImagePath, Stretch.None);
CurrentStretch = StretchList[0];
}
My Xaml looks like this:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Row="0" Grid.Column="0" >
<Grid.Resources>
<local:MultiConverter x:Key="multiC"/>
</Grid.Resources>
<ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible">
<Image Source="{Binding ImagePath}" Name="PhotoImage">
<Image.Stretch>
<MultiBinding Converter="{StaticResource multiC}">
<Binding Path="ImageVM" />
<Binding RelativeSource="{RelativeSource AncestorType=Window}" Path="ActualWidth"/>
<Binding RelativeSource="{RelativeSource AncestorType=Window}" Path="ActualHeight"/>
</MultiBinding>
</Image.Stretch>
<Image.LayoutTransform>
<ScaleTransform ScaleX="{Binding ImageVM.Scale}" ScaleY="{Binding ImageVM.Scale}"
CenterX="0.5" CenterY="0.5" />
</Image.LayoutTransform>
</Image>
</ScrollViewer>
</Grid>
<ComboBox Grid.Row="2" Grid.Column="0" ItemsSource="{Binding StretchList}" SelectedItem="{Binding CurrentStretch}" DisplayMemberPath="."/>
</Grid>
As you can see, I've used a multi-value converter that takes 3 arguments: current image view-model and window width and height. This arguments were used to calculate current size of the area that image fills. Also I've used ScaleTransform to scale that area to the calculated size. This is the code for multi-value converter:
public class MultiConverter : IMultiValueConverter
{
public object Convert(
object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values[0] is ImageViewModel)
{
var imageVm = (ImageViewModel)values[0];
// if user selects UniformToFill
if (imageVm.Stretch == Stretch.UniformToFill)
{
var windowWidth = (double)values[1];
var windowHeight = (double)values[2];
var scaleX = windowWidth / (double)imageVm.Width;
var scaleY = windowHeight / (double)imageVm.Height;
// since it's "uniform" Max(scaleX, scaleY) is used for scaling in both horizontal and vertical directions
imageVm.Scale = Math.Max(scaleX, scaleY);
// "UniformToFill" is actually "Uniform + Proper Scaling"
return Stretch.Uniform;
}
// if user selects other stretch types
// remove scaling
imageVm.Scale = 1;
return imageVm.Stretch;
}
return Binding.DoNothing;
}
public object[] ConvertBack(
object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
So ultimately i took a discussion with some co-workers and we agreed that we need to fix the problem before a fix. In other words replace Stretch attribute combined with scrollviewer with something more robust that will support extent ability.
The solution I came up with will work for now, and a better solution to the whole problem will be preformed next scrum sprint.
Solution
A custom dependencyproperty that will control width and height depending on stretch attribute currently present on element.
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Column="2" Grid.Row="0" Grid.RowSpan="2">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Image Name="PhotoImage"
Source="{Binding SelectedPhoto.Value.Image}"
Stretch="{Binding ImageStretch, NotifyOnTargetUpdated=True}}"
extensions:ImageExtensions.ChangeWidthHeightDynamically="True"/>
</ScrollViewer>
</Grid>
Dependency Property
public static bool GetChangeWidthHeightDynamically(DependencyObject obj)
{
return (bool)obj.GetValue(ChangeWidthHeightDynamicallyProperty);
}
public static void SetChangeWidthHeightDynamically(DependencyObject obj, bool value)
{
obj.SetValue(ChangeWidthHeightDynamicallyProperty, value);
}
public static readonly DependencyProperty ChangeWidthHeightDynamicallyProperty =
DependencyProperty.RegisterAttached("ChangeWidthHeightDynamically", typeof(bool), typeof(ImageExtensions), new PropertyMetadata(false, OnChangeWidthHeightDynamically));
private static void OnChangeWidthHeightDynamically(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var image = d as Image;
if (image == null)
return;
image.SizeChanged += Image_SizeChanged;
image.TargetUpdated += Updated;
}
private static void Updated(object sender, DataTransferEventArgs e)
{
//Reset Width and Height attribute to Auto when Target updates
Image image = sender as Image;
if (image == null)
return;
image.Width = double.NaN;
image.Height = double.NaN;
}
private static void Image_SizeChanged(object sender, SizeChangedEventArgs e)
{
var image = sender as Image;
if (image == null)
return;
image.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
if (Math.Abs(image.ActualHeight) <= 0 || Math.Abs(image.ActualWidth) <= 0)
return;
switch (image.Stretch)
{
case Stretch.Uniform:
{
image.Width = Double.NaN;
image.Height = Double.NaN;
break;
}
case Stretch.None:
{
image.Width = image.RenderSize.Width;
image.Height = image.RenderSize.Height;
break;
}
case Stretch.UniformToFill:
{
image.Width = image.ActualWidth;
image.Height = image.ActualHeight;
break;
}
default:
{
image.Width = double.NaN;
image.Height = double.NaN;
break;
}
}
}
The problem may come from the rest of your layout - If the Grid is contained in an infinitely resizable container (a Grid Column/Row set to Auto, a StackPanel, another ScrollViewer...), it will grow with the Image. And so will do the ScrollViewer, instead of activating the scroll bars.

How to Hide/enable a Column in userControl when calling that control in other window?

I have a User control. It has Some textboxes. I need to hide a single column in that control and if require I need to set as visible. It's like, setting visibility property to a textbox visible/hidden/collapsed. Same thing I need to do in a Column for a user control.
Here is my code.
UserControl Xaml:
<UserControl x:Class="UserControls.UserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<Grid.RowDefinitions >
<RowDefinition Height="45"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="15*" />
<ColumnDefinition Width="10*" />
<ColumnDefinition Width="10*" />
</Grid.ColumnDefinitions>
<TextBox Name="txt1"
Text=""
Grid.Row="0" Grid.Column="0"/>
<TextBox Name="txt1"
Text=""
Grid.Row="0" Grid.Column="1"/>
//....some othr controls..
</Grid>
</UserControl>
Window1.cs:
public partial class Window1
{
public Window1()
{
InitializeComponent();
var uc = new UserControl();
grid1.RowDefinitions.Add(new RowDefinition());
Grid.SetRow(uc, grid1.RowDefinitions.Count - 1);
grid1.Children.Add(uc);
}
}
I need to hide the Column 1. How might I conceal this column and if require I need to Enable this column too. Any offer assistance??
Column cannot be hidden by itself. Nevertheless you can wrap elements in column to one panel and then set its visibility to hidden/collapsed. You can also select all UIelements from column and subsequently set their visibility
var elements = Grid.Children.OfType<FrameworkElement>().Where(x => Grid.GetColumn(x) == ColumnNumber).ToList();
elements.ForEach(x => x.Visibility = System.Windows.Visibility.Collapsed);
EDIT
I created Attached Properties for you problem. I have simple grid panel with 2 elements inside
<Grid local:MyGrid.IsHidden="True" local:MyGrid.ColumnNumber="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="1" Grid.Column="0"/>
<TextBlock Text="2" Grid.Column="1" Name="tekst"/>
</Grid>
class MyGrid looks as follows:
public class MyGrid
{
public static void SetIsHidden(DependencyObject obj, bool val)
{
obj.SetValue(IsHiddenProperty, val);
}
public static bool GetIsHidden(DependencyObject obj)
{
return (bool)obj.GetValue(IsHiddenProperty);
}
public static void SetColumnNumber(DependencyObject obj, int val)
{
obj.SetValue(ColumnNumberProperty, val);
}
public static int GetColumnNumber(DependencyObject obj)
{
return (int)obj.GetValue(ColumnNumberProperty);
}
public static readonly DependencyProperty IsHiddenProperty = DependencyProperty.RegisterAttached("IsHidden", typeof(bool), typeof(MyGrid));
public static readonly DependencyProperty ColumnNumberProperty = DependencyProperty.RegisterAttached("ColumnNumber", typeof(int), typeof(MyGrid),
new FrameworkPropertyMetadata(-1, new PropertyChangedCallback((x, y) =>
{
if (x is Grid && GetIsHidden(x))
((Grid)x).Loaded += MyGrid_Loaded;
})));
static void MyGrid_Loaded(object sender, RoutedEventArgs e)
{
if (GetColumnNumber((DependencyObject)sender) >= 0 && GetColumnNumber((DependencyObject)sender) <= ((Grid)sender).ColumnDefinitions.Count - 1)
{
var elements = ((Grid)sender).Children.OfType<FrameworkElement>().Where(z => Grid.GetColumn(z) == GetColumnNumber((DependencyObject)sender)).ToList();
elements.ForEach(s => s.Visibility = System.Windows.Visibility.Collapsed);
}
}
}
Now if you set local:MyGrid.IsHidden to True and insert valid ColumnNumber local:MyGrid.ColumnNumber all UI elements will be hidden.
Standard view
If you set
local:MyGrid.IsHidden="True" local:MyGrid.ColumnNumber="0"
For settings
local:MyGrid.IsHidden="False" local:MyGrid.ColumnNumber="0"
everyting stays normal
As you can see from the ColumnDefinition Class page on MSDN, there is no Visibility property. Therefore, it is not possible to set the Visibililty of a Grid.Column. The customary way to do this would be to set the Visibility on each of the controls in that column.
The standard way to set the Visibility on one or more controls from other controls is to provide one or more bool properties:
public bool IsVisible { get; set; } // Implement INotifyPropertychanged interface here
Then we can data bind that property to the Control.Visibillity property of the relevant controls:
<TextBox Name="txt1" Text="" Grid.Row="0" Grid.Column="0" Visibillity="{
Binding IsVisible, Converter={StaticResource BooleanToVisibilityConverter}}" />
You can use the same Binding on each of the controls in the column, so that they can all be hidden at once:
// Hide column controls
IsVisible = false;
// Show column controls
IsVisible = true;
Finally, if you create a DependencyProperty for the IsVisible property, then you would be able to data bind to it from outside of your UserControl:
<Local:YourUserControl IsVisible="{Binding IsVisibleInMainWindowDataContext}" />
Then from object set as MainWindow.DataContext:
// Hide column controls
IsVisibleInMainWindowDataContext = false;
// Show column controls
IsVisibleInMainWindowDataContext = true;

Listbox ScrollIntoView doesn't work for variable height items

I have a listbox that various items are added to. When a new item is added to the listbox, I need to scroll that item into view (basically scroll to the bottom).
I've tried the solution from How can I have a ListBox auto-scroll when a new item is added? and also from this blog post
However, neither solutions work because my listbox contains variable height items. If I hack my listbox items templates to have a fixed height instead, then it seems to work. Here is an example of one of my item templates:
<DataTemplate x:Key="StatusMessageTemplate">
<Grid Grid.Column="1" VerticalAlignment="top" Margin="0,5,10,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="20"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Text="{Binding Path=MessageText}" HorizontalAlignment="Left" Grid.Row="0" Grid.Column="0" FontWeight="Bold" Foreground="{DynamicResource LightTextColorBrush}"/>
<TextBlock Text="{Binding Path=created_at, StringFormat=t}" Style="{StaticResource Timestamp}" TextWrapping="Wrap" HorizontalAlignment="Right" Grid.Row="0" Grid.Column="1"/>
</Grid>
</DataTemplate>
How can I make the new items scroll into view regardless of their height?
I need to scroll that item into view (basically scroll to the bottom).
ScrollIntoView behaves strange when the Listbox has variable height items.
If the sole purpose is to scroll to the bottom, you can access the Scrollviewer directly and scroll to the maximum possible offset as shown below.
var scrollViewer = GetDescendantByType(ListBoxChats, typeof(ScrollViewer)) as ScrollViewer;
scrollViewer.ScrollToVerticalOffset(Double.MaxValue);
public static Visual GetDescendantByType(Visual element, Type type)
{
if (element == null)
{
return null;
}
if (element.GetType() == type)
{
return element;
}
Visual foundElement = null;
if (element is FrameworkElement)
{
(element as FrameworkElement).ApplyTemplate();
}
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
{
Visual visual = VisualTreeHelper.GetChild(element, i) as Visual;
foundElement = GetDescendantByType(visual, type);
if (foundElement != null)
{
break;
}
}
return foundElement;
}
GetDescendantByType is an helper function written by punker76 # another SO post
I think I have found the problem. Variable height items are not calculated until displayed. So I add a timer to call the ScrollIntoView function. But even that didn't work well, so I used the VisualTreeHelper to find the ScrollViewer object and force it to the specific row. Here is the code.
System.Windows.Threading.DispatcherTimer dTimer = new System.Windows.Threading.DispatcherTimer();
dTimer.Interval = new TimeSpan(0, 0, 0, 0, 200); // 200 Milliseconds
dTimer.Tick += new EventHandler(
(seder, ea) =>
{
//Verses.ScrollIntoView(Verses.Items[itemIndex]);
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(Verses); i++)
{
DependencyObject depObj = VisualTreeHelper.GetChild(Verses, i);
if (depObj is ScrollViewer)
{
ScrollViewer sv = depObj as ScrollViewer;
sv.ScrollToVerticalOffset(itemIndex); // Zero based index
break;
}
}
dTimer.Stop();
});
dTimer.Start();

Resources