TreeView jumpy scrolling - wpf

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.

Related

Vertical ScrollBar for subnodes only

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.

Scale usercontrol in wpf ? vertical and horizontal scrollbars

I implemented a usercontrol, but i have a problem with rescaling the window. I know when i make the window smaller, everything scales, and every textbox and label becomes also smaller. But this is not what i want, i just want that when i make the screen smaller, everything stays the same size, and that scrollbars appear ( vertical and horizontal ). How do i do that?
Thanks
Assumption
The behaviour you describe, is not mandatory the default layout-behaviour of WPF. It depends on the layout-controls you use. I assume, you're using a Grid with setting it's columns and rows to the Start (*)-GridLengths. This would have more or less such an effect as you describe (without scaling). Or maybe you are using a ViewBox, this control scales the whole content based on the available layout size.
Solution
I guess that wrapping your whole content into a ScrollViewer will probably do what you desire. If not, I suggest that you post some XAML-code to show us how you have built your content.
<ScrollViewer>
<YourContent>
</YourContent>
</ScrollViewer>
Update
If you really scale your window (applying a ScaleTransformation) and you want your UserControl to be the only control within that does not scale, you have to scale your UserControl in the opposite direction as you have done it with your window. Apply a ScaleTransformation and set the scale values to 1/scale. Or try to use the ViewBox to blow up the content of your UserControl, but this will not be very exact.

ScrollViewer's Viewport Height VS Actual Height [duplicate]

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.

How to calculate a bounding box for an Expander?

I have an Expander control and i need to calculate its bounds without invisible elements and margins. It commonly can be done by VisualTreeHelper.GetDescendantsBounds. But it seems that the rect is calculated by VisualTreeHelper doesn't depend on the expander state. For example:
http://i.piccy.info/i5/58/39/273958/collapsed.jpg
(i can't post images. sorry)
The same result as for expanded state (light green rectangle on the image). Does anybody know how to solve this problem?
The Expander control will set its content's visibility to Collapsed, which means it won't be considered during layout and won't be included in GetDescendantBounds. However, the Expander can be forced to have a larger size by the layout engine, and the Expander's own size is included in GetDescendantBounds.
Try setting VerticalAlignment="Top" on the Expander. The default is Stretch, which will allow it to increase in size if the parent has more space available. Also make sure you aren't explicitly setting the Height property.
This is the sample application. The style is applied here to the TreeView control and its items. But the problem doesn't depend on the style.

WPF DataGrid : CanContentScroll property causing odd behavior

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.

Resources