I have a problem where a node in a TreeView has so many subnodes that they can't be displayed all at once on the screen. on the right side i have a scrollbar for the whole treeview. but when i scroll it down, it moves the treeview to the next node - not a subnode. It means that those subnodes outside of visible area can never be displayed. i wonder if it's possible to add one extra scrollbar that would only serve for scrolling subnodes a node. It would appear only if a node has more subnodes than it is possible to display on the screen at once.
It sounds to me like you need to take a look at the ScrollViewer.CanContentScroll Property. From the linked page:
Content in a ScrollViewer can be scrolled in terms of physical units or logical units. Physical units are device independent pixels. Logical units are used for scrolling items within an ItemsControl. The default behavior of the ScrollViewer is to use physical units to scroll its content. However, in cases where the CanContentScroll is set to true, the content could use logical units to scroll. For example, ListBox, ListView, and other controls that inherit from ItemsControl use logical units to scroll. If CanContentScroll is true, the values of the ExtentHeight, ScrollableHeight, ViewportHeight, and VerticalOffset properties are number of items, instead of physical units.
If you require physical scrolling instead of logical scrolling, wrap the host Panel element in a ScrollViewer and set its CanContentScroll property to false. Physical scrolling is the default scroll behavior for most Panel elements.
So just try adding ScrollViewer.CanContentScroll="False" to the declaration of your TreeView.
Related
I have a user control which paints content in OnRender. The Height of that control grows by and by.
I added this control to a ScrollViewer. The control does only repaint the currently visible area (viewport) (+/- a view lines for smoother scrolling).
Everything works fine so far...
But since the control usualy grows up to a few hundred thousands of pixels I want to keep the Height of my control as small as possible and provide a different Height value to bind to the ScrollableHeight of the ScrollViewer (same goes for VerticalOffset). But there is no setter for ScrollableHeight. It binds automatically to the Height of my Control. Neither can I override Height.
How can I customize my ScrollViewer (or VerticalScrollbar) to keep the real Height of my control small?
I did something like this in the past. What you need to do is to write your own layout Panel and implement the IScrollInfo in it. The interface looks big, but most of it is just calling one of the main set methods. The layouter needs to set some of the IScrollInfo properties, like ExtentHeight, Offset etc. and these are your way to customize how the ScrollViewer will calculate the scroll position and the scrollable area for your "virtual" canvas size. For implementing the IScrollInfo i used this tutorial as a guidance.
I have a virtualized TreeView with these properties:
<TreeView
ScrollViewer.CanContentScroll="True"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.IsVirtualizingWhenGrouping="True"
VirtualizingStackPanel.ScrollUnit="Pixel"
VirtualizingStackPanel.CacheLength="20,20"
VirtualizingStackPanel.CacheLengthUnit="Item"
/>
...
</TreeView>
The TreeViewItem's are roughly the same height.
The problem is that when scrolling down the handle of the scrollbar changes size and the scrolling is "jumpy" which leads to a quite poor user experience.
How might one obtain a more smooth UX?
I realize that this is an old post and you probably have a solution now, but it has a simple answer, so for anyone who may come across this in the future, here it is:
The behaviour you describe is normal. Had you search in MSDN for ScrollViewer.CanContentScroll which is the very first property that you used, you would have found this:
Content in a ScrollViewer can be scrolled in terms of physical units or logical units. Physical units are device independent pixels. Logical units are used for scrolling items within an ItemsControl. The default behavior of the ScrollViewer is to use physical units to scroll its content. However, in cases where the CanContentScroll is set to true, the content could use logical units to scroll. For example, ListBox, ListView, and other controls that inherit from ItemsControl use logical units to scroll. If CanContentScroll is true, the values of the ExtentHeight, ScrollableHeight, ViewportHeight, and VerticalOffset properties are number of items, instead of physical units.
If you require physical scrolling instead of logical scrolling, wrap the host Panel element in a ScrollViewer and set its CanContentScroll property to false. Physical scrolling is the default scroll behavior for most Panel elements.
So, this basically means that you can either scroll in terms of pixels, or whole items from the collection. Therefore, to have smooth scrolling, you'll want to scroll using pixels. To do that, you simply need to set the ScrollViewer.CanContentScroll property to False. So it turns out that it is an easy fix after all.
Unfortunately, setting ScrollViewer.CanContentScroll = False will disable virtualization so doesn't really help with this problem.
I have found that setting a hard coded height value to the content of your TreeViewItem makes the scrollbar behave a lot better, but the scroll bar still resizes a bit.
This question already has answers here:
WPF DataGrid : CanContentScroll property causing odd behavior
(2 answers)
Closed 9 years ago.
Both are quite general terms but I'm curious to know when these height will be different apart from the case we're using Virtualization?
One more question:
I read on MSDN:
If CanContentScroll is true, the values of the ExtentHeight, ScrollableHeight, ViewportHeight, and VerticalOffset properties are number of items. If CanContentScroll is false, the values of these properties are Device Independent Pixels.
However I'm facing an issue with ViewPort Height: I've 2 listbox in application:
1. Which have Virtualization Enabled and CanContentScroll = True.
2. Which have no virtualization and CanContentScroll = True.
In ListBox 1 while drag-drop Viewport Height comes to 4/5 (Number of elements currently visible). However in ListBox 2 i get Viewport Height equal to Actual Height of Listbox.
Why this difference?
Few more findings:
1. Scrollable Height is number of items not visible in scrollviewer
2. Viewport Height is number of items visible in scrollviewer.
Thus Viewport Height + ScrollableHeight = Extent Height
Can someone please explain what's the difference between two listboxes? I need ViewPort hieght in case of Listbox 1
the ActualHeight is the actual height of the ScrollViewer. The Viewport is what is visible from the ScrollViewers Content. So to answer your question: ViewportHeight differs from ActualHeight if the horizontal Scrollbar is visible by the Height of the Scrollbar.
so, to sum this up:
ActualHeight = ViewportHeight + HorizontalScrollbarHeight
Finally This was the root cause:
Quoting from https://stackoverflow.com/a/3062692/3195477:
You are encountering the differences between physical scrolling and
logical scrolling.
As you have discovered, each has its tradeoffs.
Physical scrolling
Physical scrolling (CanContentScroll=false) just goes by pixels, so:
The viewport always represents exactly the same portion of your scroll
extent, giving you a smooth scrolling experience, and but
The entire contents of the DataGrid must have all templates fully
applied and be measured and arranged to determine the size of the
scrollbar, leading to long delays during loading and high RAM usage,
and It doesn't really scroll items so it doesn't understand
ScrollIntoView very well Logical scrolling
Logical scrolling (CanContentScroll=true) calculates its scroll viewport and extent by items instead of pixels, so:
The viewport may show a different number of items at different times,
meaning the number of items in the viewport as compared to the number
of items in the extent varies, causing the scrollbar length to change,
and
Scrolling moves from one item to the next and never in between,
leading to "jerky" scrolling
but
As long as you're using VirtualizingStackPanel under the hood, it only
needs to apply templates and measure and arrange the items that are
actually visible at the moment, and
ScrollIntoView is much simpler since it just needs to get the right
item index into view
Choosing between them
These are the only two kinds of scrolling provided by WPF. You must
choose between them based on the above tradeoffs. Generally logical
scrolling is best for medium to large datasets, and physical scrolling
is best for small ones.
A trick to speed loading during physical scrolling is to make the
physical scrolling better is to wrap your items in a custom Decorator
that has a fixed size and sets its child's Visibility to Hidden when
it is not visible. This prevents the ApplyTemplate, Measure and
Arrange from occuring on the descendant controls of that item until
you're ready for it to happen.
A trick to make physical scrolling's ScrollIntoView more reliable is
to call it twice: Once immediately and once in a dispatcher callback
of DispatcherPriority.ApplicationIdle.
Making logical scroll scrollbar more stable
If all your items are the same height, the number of items visible in
the viewport at any time will stay the same, causing the scroll thumb
size to stay the same (because the ratio with total number if items
doesn't change).
It is also possible to modify the behavior of the ScrollBar itself so
the thumb is always calculated to be a fixed size. To do this without
any hacky code-behind:
Subclass Track to replace the calculation of Thumb position and size in MeasureOverride with your own
Change the ScrollBar template used for the logical-scrolling ScrollBar to use your subclassed Track instead of the regular
one
Change the ScrollViewer template to explicitly set your custom ScrollBar template on the logical-scrolling ScrollBar
(instead of using the default template)
Change the ListBox template to use explicitly set your custom ScrollViewer template on the ScrollViewer it creates
This means copying a lot of template code fom the built-in WPF
templates, so it is not a very elegant solution. But the alternative
to this is to use hacky code-behind to wait until all the templates
are expanded, then find the ScrollBar and just replace the ScrollBar
template with the one that uses your custom Track. This code saves two
large templates (ListBox, ScrollViewer) at the cost of some very
tricky code.
Using a different Panel would be a much larger amount of work:
VirtualizingStackPanel is the only Panel that virtualizes, and only it
and StackPanel to logical scrolling. Since you are taking advantage of
VirtualizingStackPanel's virtualization abilities you would have to
re-implement all of these plus all IScrollInfo info function plus your
regular Panel functions. I could do something like that but I would
allocate several, perhaps many, days to get it right. I recommend you
not try it.
They can differ from the point of (specified) Height being evaluated to any given time during the (ongoing) rendering process.
From MSDN:
There is a difference between the
properties of Height and Width and
ActualHeight and ActualWidth. For
example, the ActualHeight property is
a calculated value based on other
height inputs and the layout system.
The value is set by the layout system
itself, based on an actual rendering
pass, and may therefore lag slightly
behind the set value of properties,
such as Height, that are the basis of
the input change.
Because ActualHeight
is a calculated value, you should be
aware that there could be multiple or
incremental reported changes to it as
a result of various operations by the
layout system. The layout system may
be calculating required measure space
for child elements, constraints by the
parent element, and so on.
I have a solution where I generate a DataGrid (or multiple instances) based on user criteria. Each grid keeps receiving data as it comes in via an ObservableCollection.
The problem I had was that the scroll acted weird. It was choppy, and scrollbar would resize itself while scrolling.
Then I found the CanContentScroll property! It completely fixes the weird scrolling behavior bringing me temporary bliss and happiness.
However, it causes two unfortunate side effects:
Whenever I re-create DataGrid instances and bind them to my ObservableCollection, it freezes my entire window for 5 seconds. When my DataGrid grows to a big size, this delay can last for 30 seconds.
When I call TradeGrid.ScrollIntoView(TradeGrid.Items(TradeGrid.Items.Count - 1)) to scroll to the bottom, it jumps to bottom and then back to the top.
Is there another way to achieve smooth scrolling perhaps?
You are encountering the differences between physical scrolling and logical scrolling.
As you have discovered, each has its tradeoffs.
Physical scrolling
Physical scrolling (CanContentScroll=false) just goes by pixels, so:
The viewport always represents exactly the same portion of your scroll extent, giving you a smooth scrolling experience, and
but
The entire contents of the DataGrid must have all templates fully applied and be measured and arranged to determine the size of the scrollbar, leading to long delays during loading and high RAM usage, and
It doesn't really scroll items so it doesn't understand ScrollIntoView very well
Logical scrolling
Logical scrolling (CanContentScroll=true) calculates its scroll viewport and extent by items instead of pixels, so:
The viewport may show a different number of items at different times, meaning the number of items in the viewport as compared to the number of items in the extent varies, causing the scrollbar length to change, and
Scrolling moves from one item to the next and never in between, leading to "jerky" scrolling
but
As long as you're using VirtualizingStackPanel under the hood, it only needs to apply templates and measure and arrange the items that are actually visible at the moment, and
ScrollIntoView is much simpler since it just needs to get the right item index into view
Choosing between them
These are the only two kinds of scrolling provided by WPF. You must choose between them based on the above tradeoffs. Generally logical scrolling is best for medium to large datasets, and physical scrolling is best for small ones.
A trick to speed loading during physical scrolling is to make the physical scrolling better is to wrap your items in a custom Decorator that has a fixed size and sets its child's Visibility to Hidden when it is not visible. This prevents the ApplyTemplate, Measure and Arrange from occuring on the descendant controls of that item until you're ready for it to happen.
A trick to make physical scrolling's ScrollIntoView more reliable is to call it twice: Once immediately and once in a dispatcher callback of DispatcherPriority.ApplicationIdle.
Making logical scroll scrollbar more stable
If all your items are the same height, the number of items visible in the viewport at any time will stay the same, causing the scroll thumb size to stay the same (because the ratio with total number if items doesn't change).
It is also possible to modify the behavior of the ScrollBar itself so the thumb is always calculated to be a fixed size. To do this without any hacky code-behind:
Subclass Track to replace the calculation of Thumb position and size in MeasureOverride with your own
Change the ScrollBar template used for the logical-scrolling ScrollBar to use your subclassed Track instead of the regular one
Change the ScrollViewer template to explicitly set your custom ScrollBar template on the logical-scrolling ScrollBar (instead of using the default template)
Change the ListBox template to use explicitly set your custom ScrollViewer template on the ScrollViewer it creates
This means copying a lot of template code fom the built-in WPF templates, so it is not a very elegant solution. But the alternative to this is to use hacky code-behind to wait until all the templates are expanded, then find the ScrollBar and just replace the ScrollBar template with the one that uses your custom Track. This code saves two large templates (ListBox, ScrollViewer) at the cost of some very tricky code.
Using a different Panel would be a much larger amount of work: VirtualizingStackPanel is the only Panel that virtualizes, and only it and StackPanel to logical scrolling. Since you are taking advantage of VirtualizingStackPanel's virtualization abilities you would have to re-implement all of these plus all IScrollInfo info function plus your regular Panel functions. I could do something like that but I would allocate several, perhaps many, days to get it right. I recommend you not try it.
I also have same issue with my DataGrid and finally I did:
ScrollViewer.CanContentScroll="True"
EnableRowVirtualization="True"
VirtualizingPanel.VirtualizationMode="Standard"
Now everything is working fine in my DataGrid.
I have read that placing a canvas inside a scrollviewer won't work because the canvas does not report its size. I have been experimenting with different containers (borders, grids, canvases and scrollviewers) and could really do with a simple explanation of how scrollviewers behave within nested containers. e.g. If I have a container hierarchy of UserControl>Grid1>Canvas1>ScrollViewer>Grid2>Canvas2 should scrollbars appear around Grid2 when Canvas2 becomes wider than Canvas1? (or indeed wider than UserControl) If not, how should I organise my containers so I can add loads of uielements to Canvas2 and have scrollbars appear as necessary. (My usercontrol width and height are set to 100%)
Canvas will work with a ScrollViewer if you explicitly give it a size. The problem comes from the fact that if you don't supply a Height and Width for any control it will try and determine it's available area based off it's parent container. A ScrollViewer however has infinite available area.
Take a Grid for example. If I define a Grid that has 2 Star Width columns and 2 Star Height rows. How does the Grid know how wide each of those columns should be? The star says they should be half of the available area, but inside a ScrollViewer the available area is infinity.
What controls are you adding to Canvas2? If you are adding them with fixed positions than the ScrollViewer will expand to house all elements. If you aren't giving them fixed positions than all the controls will stack on top of each other, because that is the arrange behavior of a Canvas