I am currently working on my first WPF project and trying to make a ListView scrollable.
At first I thought this could be easily done by simply limiting the ListView's width and height and thus forcing a scrollbar to appear automatically whenever the content exceeds its space. This seemed fine at first but due to the handled PreviewMouseDown event (which enables dragging the list's items) it doesn't work after selecting an item.
Second attempt (using ScrollViewer)
<ScrollViewer>
<ListView ItemsSource="{Binding FileViewModels}"
PreviewMouseDown="ListView_MouseMove"
Height="450" Width="200"/>
</ScrollViewer>
Of course, this resulted in a second scrollbar whenever the list's content became larger than its max height. And dragging the bar still didn't work after selecting an item.
Third (quite foolish) attempt (disabling scrollbar duplicate)
<ScrollViewer>
<ListView ItemsSource="{Binding FileViewModels}"
PreviewMouseDown="ListView_MouseMove"
Height="450" Width="200"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"/>
</ScrollViewer>
This removed the scrollbar duplicate and enabled scrolling via mouse wheel but disabled the scrollbar, so you couldn't move by clicking and dragging it.
Fourth attempt (constant size of the ScrollViewer)
<ScrollViewer Height="450" Width="200">
<ListView ItemsSource="{Binding FileViewModels}"
PreviewMouseDown="ListView_MouseMove"/>
</ScrollViewer>
Removed the width/height constraint from the ListView and moved it to the ScrollViewer. This enables the scrollbar and removes the duplicate. Unfortunately the mouse wheel doesn't work anymore (dragging the scroll bar works fine).
Could somebody please explain to me why the mouse wheel doesn't work anymore and how to fix this?
Edit
Maybe I should go back to my first solution.
Obviously, the ListView's template already contains a ScrollViewer. The remaining problem would then be that I cannot drag the scrollbar after selecting an item because of the handled PreviewMouseDown event (scrolling via MouseWheel still works in that case). Should I handle the dragging of items differently (it worked fine for me, before wanting to add a scrollbar)? Or is there a way to detect if the cursor is above the scrollbar (so I could then deselect the item which enables scrolling)?
Or are there any other suggestions?
This may help you..
private void ListViewScrollViewer_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
{
ScrollViewer scv = (ScrollViewer)sender;
scv.ScrollToVerticalOffset(scv.VerticalOffset - e.Delta);
e.Handled = true;
}
This would probably be the most comfortable solution:
<ListView.Template>
<ControlTemplate>
<ScrollViewer>
<ItemsPresenter></ItemsPresenter>
</ScrollViewer>
</ControlTemplate>
</ListView.Template>
For me this worked:
<ListView.Template>
<ControlTemplate>
<!-- Empty template to allow ScrollViewer to capture mouse scroll -->
<ItemsPresenter />
</ControlTemplate>
</ListView.Template>
instead of this:
<ListView.Template>
<ControlTemplate>
<ScrollViewer>
<ItemsPresenter></ItemsPresenter>
</ScrollViewer>
</ControlTemplate>
</ListView.Template>
<ScrollViewer Background="Transparent">
If Background is null, the mouse wheel will not work on ScrollViewer. You can set the Background to Transparent or some other value.
In my case this helped:
<ScrollViewer ScrollViewer.CanContentScroll="True" ScrollViewer.VerticalScrollBarVisibility="Disabled" ScrollViewer.HorizontalScrollBarVisibility="Auto" >
<DataGrid x:Name="dataGrid" SelectionMode="Single" ItemsSource="{Binding}" SelectedValuePath="{Binding Item}" AutoGenerateColumns="True">
</DataGrid>
</ScrollViewer>
The design was disabling VerticalScrollBarVisibility attribute in outer scope , i.e. in ScrollViewer.
I want to add some comment to the solution Rocky provided. It worked fine for me, but later I needed to use it in a different window to scroll Grid. I faced a problem: the ScrollViewer did not scroll to the bottom end. The reason was because of attempts to set the invalid VerticalOffset value. The code below works fine for me (just need to change PreviewMouseWheel handler:
private void UIElement_OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
ScrollViewer scroll = (ScrollViewer)sender;
if (e.Delta < 0)
{
if (scroll.VerticalOffset - e.Delta <= scroll.ExtentHeight - scroll.ViewportHeight)
{
scroll.ScrollToVerticalOffset(scroll.VerticalOffset - e.Delta);
}
else
{
scroll.ScrollToBottom();
}
}
else
{
if (scroll.VerticalOffset + e.Delta > 0)
{
scroll.ScrollToVerticalOffset(scroll.VerticalOffset - e.Delta);
}
else
{
scroll.ScrollToTop();
}
}
e.Handled = true;
}
Related
Task
My task was to be able to change the order of the service cards in the ListBox by dragging and dropping. I found a good solution on StackOverflow that has a lot of useful features (gong-wpf-drag drop, https://stackoverflow.com/a/33367826/19631476).
Problem
However, I had to face an unpleasant visual bug: when I drag the service card, there is some label that indicates where the dragged object will be moved. So, for some reason, this label is clipped if a number of objects are also clipped and not visible in the ListBox. However, when scrolling, the cropping does not disappear. Moreover, then the label will always point to the line above, wherever it is.
Problem demo
below I will present some screenshots demonstrating the problem
the normal state of the separator
the separator is cut off due to the fact that the card does not fit into the container
the separator continues to be cut off after scrolling
the separator is shifted up a line, and is still clipped, despite the fact that I move the elements already in another line
My code
This is how my ListBox element looks like:
<ListBox
Margin="10 0 10 10"
Background="Cornsilk"
MinWidth="300"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
dd:DragDrop.IsDragSource="True"
dd:DragDrop.IsDropTarget="True"
dd:DragDrop.SelectDroppedItems="True"
ItemsSource="{Binding ServicesListCollectionView.View}"
SelectedIndex="{Binding SelectedServiceIndex}"
SelectedItem="{Binding SelectedService}"
d:ItemsSource="{d:SampleData ItemCount=5}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Border AllowDrop="True" Style="{StaticResource BorderStyle}">
<DockPanel Background="GhostWhite">
<!-- Service Card Template-->
</DockPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
it contains a custom container so that the service cards can be arranged as a grid from top to bottom
What have I tried to do?
The idea came to my mind to prohibit the use of ScrollBar for the ListBox element. It worked, and the visual bug sort of disappeared. But you can't leave it like that. I wrapped the ListBox in ScrollViewer and allowed it to scroll vertically. This visual bug is no longer there:
code:
<ScrollViewer VerticalScrollBarVisibility="Auto" x:Name="ServiceScroll">
<!-- Here is ListBox -->
</ScrollViewer>
However, this solution is not suitable:
I lose control of scrolling in the ListBox and can only use scrolling on the ScrollViewer itself
Scrolling also becomes unavailable when dragging items.
the second problem can be solved by specifying the dd:DragDrop.DropTargetScrollViewer ScrollViewer with binding to the ScrollViewer element:
<ScrollViewer VerticalScrollBarVisibility="Auto" x:Name="ServiceScroll">
<ListBox
...
dd:DragDrop.IsDragSource="True"
dd:DragDrop.IsDropTarget="True"
dd:DragDrop.SelectDroppedItems="True"
dd:DragDrop.DropTargetScrollViewer="{Binding ElementName=ServiceScroll}"
...
></ListBox>
</ScrollViewer>
but in this case, the visual bug described above appears again, and also, this solution still does not allow you to scroll through the elements inside the ListBox using the mouse wheel.
Any ideas?
To be honest, I do not know what to do. Has anyone encountered anything described above?
Maybe I'm doing something wrong, which is why I get such strange behavior? What's wrong with scrolling?
Maybe you have good solutions to solve the problem? How else would it be possible to solve the issue of dragging items to the ListBox in the most flexible way?
Is there a way to visually indicate that there is scrollable content below the visible area of a <ScrollViewer>?
I see this effect in applications like Microsoft Teams. A shadow appears at the bottom of the scrollable area to indicate that more content is present.
None of the properties of <ScrollViewer> seem to be a clear match. But I'm hoping I can avoid having to programmatically show/hide an element below the <ScrollViewer> based on the scroll position.
I have to say that currently there is no built-in API in ScrollViewer that could directly show a shadow there if the content is not ended.
You might still need to check it programmatically by handling the ViewChanged event of the ScrollViewer and add a custom element.
You could use the following code to check if the ScrollViewer reaches the end:
<Grid>
<ScrollViewer Name="MyScrollViewer" ViewChanged="MyScrollViewer_ViewChanged">
<Rectangle
x:Name="MyRectangle"
Width="3000"
Height="3000"
Fill="Blue"
Margin="20" />
</ScrollViewer>
</Grid>
Code behind:
private void MyScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
var verticalOffset = MyScrollViewer.VerticalOffset;
var maxVerticalOffset = MyScrollViewer.ScrollableHeight;
if (maxVerticalOffset < 0 || verticalOffset == maxVerticalOffset)
{
// content reaches the end
MyRectangle.Fill = new SolidColorBrush(Colors.Red);
}
else
{
// content does not reach the end
MyRectangle.Fill = new SolidColorBrush(Colors.Blue);
}
}
I build a user control that should show a list of status indicators at the bottom of our application. I use an ItemsControl to create the list and enabled it to scroll horizontally via mouse wheel but also added two buttons (Left and Right) to replace the usual scroll bar (because the scrollbar would conflict with the look and feel and would be to big in this situation).
If there are not enough Items in the List or the control is wide enough to show all items, I want to collapse the buttons and of course show them again as soon as scrolling is needed.
Currently I use the ScrollViewer's SizeChanged event to detect if the width has changed and if any items of the ItemsControl are not visible so it needs scrolling. (See C# Code at the bottom of the post.)
My Issue is that it works fine as long as I change the size by gabbing the edge of the window with the mouse and resize it that way but it does not work as soon as the window size is programmaticaly changed or by double clicking on the window title to make it full screen (or use the maximize/minimize button).
This is my UserControl:
<Grid x:Name="grid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<dx:SimpleButton x:Name="scroll_left_button" Grid.Column="0" Content="←"/>
<Border Background="#FF00ACDC" Grid.Column="1" BorderThickness="0">
<ItemsControl x:Name="items_control" w3ClientUi:ScrollViewerHelper.UseHorizontalScrolling="True"
ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.VerticalScrollBarVisibility="Disabled"
ScrollViewer.CanContentScroll="True" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Template>
<ControlTemplate TargetType="ItemsControl">
<ScrollViewer HorizontalScrollBarVisibility="Hidden">
<ItemsPresenter/>
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<!-- dummy template -->
<StackPanel.ToolTip>
<TextBlock Text="{Binding Name}"/>
</StackPanel.ToolTip>
<panda:PndLabel Padding="0 10 2 10" Content="{Binding Number}" FontSize="16" FontWeight="Normal"
Foreground="White" Margin="0 0 2 0"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
<!-- ItemsSource is set in Code -->
</ItemsControl>
</Border>
<dx:SimpleButton Grid.Column="2" x:Name="scroll_right_button" Content="→"/>
</Grid>
In Code behind I get the ScrollViewer instance from the ItemsControl as soon as it's loaded and then attach to the SizeChanged event.
When changing the window size via the window title or programmaticaly the event is thrown but the ScrollableWidth is not updated yet and therefore my buttons are still visible in full screen (or still collapsed when it gets smaller).
scroll_viewer.SizeChanged += (s, e) =>
{
if (!e.WidthChanged) return;
if (scroll_viewer?.ScrollableWidth > 0)
{
scroll_left_button.Visibility = Visibility.Visible;
scroll_right_button.Visibility = Visibility.Visible;
}
else
{
scroll_left_button.Visibility = Visibility.Collapsed;
scroll_right_button.Visibility = Visibility.Collapsed;
}
};
So, after coming back from the weekend with a fresh mind I figured that, in my case, handling the SizeChanged event is the wrong approach since all I actually need to know is if there are any items of my ItemsControl that need scrolling and if the number has changed.
In my question I already checked the ScrollViewer's ScrollableWidth property. It shows how many items are not visible and need scrolling, so it would be enough to check if it has changed and if it's new Value is greater than zero.
ScrollableWidth is a DependencyProperty so I thought about binding to it and listening to the changed event.
So what I did first is creating the new DependencyProperty on my UserControl
// Dependency Property is Private since I only need it internally
private static readonly DependencyProperty scrollable_width_property =
DependencyProperty.Register(nameof(scrollable_width), typeof(double),
typeof(MyUserControl),
new FrameworkPropertyMetadata(null) { BindsTwoWayByDefault = false, PropertyChangedCallback = property_changed_callback });
// Wrapper only has get because ScrollableWidth is read only anyway
private double scrollable_width => (double)GetValue(scrollable_width_property);
// Listening to the change
private static void property_changed_callback(DependencyObject dpo, DependencyPropertyChangedEventArgs args)
{
var o = dpo as MyUserControl;
o?.SetButtonVisibility((double)args.NewValue > 0);
}
I removed the scroll_viewer.SizeChanged event and instead created a new public method on my user control to change the button visibility.
public void SetButtonVisibility(bool visible)
{
if (scroll_viewer == null) return;
if (visible)
{
scroll_left_button.Visibility = Visibility.Visible;
scroll_right_button.Visibility = Visibility.Visible;
}
else
{
scroll_left_button.Visibility = Visibility.Collapsed;
scroll_right_button.Visibility = Visibility.Collapsed;
}
}
The last thing that has to be done is the actual binding.
I do it once, in the ItemsControl's Loaded event after obtaining the actual ScrollViewer.
var binding = new Binding(nameof(ScrollViewer.ScrollableWidth))
{
Source = scroll_viewer
};
SetBinding(scrollable_width_property, binding);
Now, whenever the ItemControl needs scrolling (or doesn't), the visibility of the buttons will change regardless of the size change. And now it also works when using the maximize/minimize buttons in the window title.
It's probably not the best implementation since it could probably better use style trigger in xaml instead of the SetButtonVisibility method but it gets the job done.
Edit:
In my case I also had to add a SetButtonVisibility(scroll_viewer.ScrollableWidth > 0); to the ItemsContorol's LoadedEvent because the callback is not triggered at startup.
I have developed an app in which I have more than one page in flipview.
now I want to stop the swipe navigation on touch only. i have used Isenabled property but
This will disable the content of the flipview as well, I just wanted to disable its navigation but allow the user to interact with its content because I have need to drag and drop and also zoom-in and zoom-out with its content.
please help me in solving the problem.
You can try this method by setting the ManipulationMode as TranslateX and put the code below inside your FlipView:
xaml:
<FlipView Width="300" Height="300" Name="MyFlipView">
<FlipViewItem ManipulationMode="TranslateX" ManipulationDelta="FlipViewItem_ManipulationDelta">
<Image Source="Assets/1.jpg" ></Image>
</FlipViewItem>
<FlipViewItem ManipulationMode="TranslateX" ManipulationDelta="FlipViewItem_ManipulationDelta">
<Image Source="Assets/2.jpg" ></Image>
</FlipViewItem>
</FlipView>
code behind:
private void FlipViewItem_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{
if (e.Delta.Translation.X != 0)
{
e.Handled = true;
}
}
In order to disable the swipe navigation of FlipView on touch without affecting the FlipViewItem content, please override the ControlTemplate and change ManipulationMode as None in ItemsPresenter and put the code below inside your FlipView:
<FlipView.Template>
<ControlTemplate>
<ItemsPresenter ManipulationMode="None"></ItemsPresenter>
</ControlTemplate>
</FlipView.Template>
I have tested it and it works OK.
I just got started with an app for WP8.1, and encounter a problem with intercepting touch move events while leaving the original event treatment in place.
What I want to do is the following:
I have a ListView in a StackPanel in a ScrollViewer in a Grid. The ScrollViewer handles vertical scrolling of the StackPanel/ListView.
Whenever a horizontal touch move appears, I'd like to get a notification so I can adjust some ui element's position based on the horizontal movement.
Sounds simple enough, but any way I tried seems to cancel the original touch move treatment by the ScrollViewer, so the vertical scrolling is not working anymore. I'd really hate to implement the whole scrolling behavior myself ...
I tried already:
Putting a ManipulationMode="TranslationX" ManipulationDelta="handleXTranslation" on the ScrollViewer. The handelXTranslation handler is never called for whatever reason.
Putting the same Manipulation information on the ListView - now the handler gets called (and all handlers of the parent ui elements), but the ScrollViewer is not handling the scrolling anymore, probably because the ListView is not propagating the event to its parents anymore.
Adding a general touch handler to the xaml class when it is loaded. Same problem - either it is not called, and if it is called the scrolling of the ScrollViewer is not done anymore.
The XAML code looks like this (stripped of some data):
<Grid x:Name="LayoutRoot" Background="Transparent" ManipulationMode="TranslationX" ManipulationDelta="gridTranslationX">
<ScrollViewer x:Name="ScrollViewer" ManipulationMode="TranslationX" ManipulationDelta="scrollViewerTranslationX">
<StackPanel x:Name="StackPanel" Orientation="Horizontal" ManipulationMode="TranslationX" ManipulationDelta="scrollViewerTranslationX">
<ListView x:Name="ListView" ManipulationMode="TranslationX" ManipulationDelta="scrollViewerTranslationX">
</ListView>
</StackPanel>
</ScrollViewer>
</Grid>
And the handlers I tried to install in code look like this:
this.AddHandler(UIElement.ManipulationDeltaEvent, new ManipulationDeltaEventHandler(genericDeltaHandler), true);
ListView.PointerMoved += new PointerEventHandler(pointerEvent);
Is there any way to just observe the manipulation events without disturbing their normal treatment?
Sometimes it helps to talk about it ;-) ...
The answer was indeed as simple as adding the System value to the ManipulationMode on the ListView:
<ListView x:Name="ListView" ManipulationMode="TranslateX,System" ManipulationDelta="translationX">