I have an ItemsControl that wraps its ItemPresenter with a ScrollViewer. That ItemPresenter displays a ListView. Therefore I have a collection within a collection.
Now, I want only the ScrollViewer to have scrolling functionality so I have gone ahead and removed the scrolling functionality from the inner ListView.
The problem is that my scrolling event is being messed up by the ListView. As soon as my finger touches the content area it selects the ListViewItems instead of scrolling.
How can I tell through routed events if the user is trying to click or scroll? and if it is scroll, how do I prevent it from selecting the ListViewItems?
<ItemsControl ItemsSource="{Binding Countries}" >
<ItemsControl.Template>
<ControlTemplate>
<ScrollViewer PanningMode="VerticalOnly">
<ItemsPresenter/>
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ListView ItemsSource="{Binding Cities}">
<ListView.Template>
<ControlTemplate>
<ItemsPresenter/>
</ControlTemplate>
</ListView.Template>
</ListView>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
There isn't any way to look into the future to see if a user is going to begin scrolling after touching the screen or simply lift their finger off immediately afterwards. A Future API would be nice, though.
Anyhow, you could just check to see if the user has moved their finger at all after touching the ListView. If so, begin treating the "touch" like a "scroll" rather than a "click" by manually scrolling the ScrollViewer and deselecting the ListView items.
Something like this:
private bool _touchDown = false;
private double _initOffset = 0;
private double _scrollDelta = 5;
private void ListView_PreviewTouchDown(object sender, TouchEventArgs e)
{
_touchDown = true;
_initOffset = e.GetTouchPoint(this).Y;
}
private void ListView_PreviewTouchMove(object sender, TouchEventArgs e)
{
if (_touchDown && Math.Abs(r.GetTouchpoint(this).Y - _initOffset) > _scrollDelta)
{
My_ScrollViewer.ScrollToVerticalOffset(r.GetTouchpoint(this).Y - _initOffset);
My_ListView.UnselectAll();
}
}
private void ListView_PreviewTouchUp(object sender, TouchEventArgs e)
{
_touchDown = false;
_initOffset = 0;
}
Disclaimer: I just wrote this in notepad. It has problems, but it gets the concept across.
Related
I have an Itemsstackpanel inside a Listview Control. I want to fire the PointerWheelChanged event whenever the User is near the edge of a page.
When i put the event on the itemsstackpanel is disables my ability to scroll by mouse wheel. If the event is on the Listview or Grid itself it only works as long as no Items are done loading in the Listview.
Is this intended behaviour or am i missing some important information?
I researched but found no lead to this problem or behaviour.
below is my XAML:
<Grid Background="Gray">
<ProgressRing x:Name="progress" IsActive="False" x:FieldModifier="public" Foreground="Black" Height="200" Width="200"/>
<ListView x:Name="ListViewControl" x:FieldModifier="public" Margin="10,10,10,10" ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.ZoomMode="Enabled" ScrollViewer.VerticalScrollBarVisibility="Visible" DataFetchSize="10" IncrementalLoadingTrigger="Edge"
IncrementalLoadingThreshold="2" ShowsScrollingPlaceholders="True" BorderThickness="1" IsItemClickEnabled="False" SelectionMode="None" PointerEntered="ListViewControl_PointerEntered">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel CacheLength="0" Orientation="Vertical" Background="White" PointerWheelChanged="Grid_PointerWheelChanged"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
</Grid>
and the code behind(not done just trying to get it to work for now):
private void ItemsStackPanel_PointerWheelChanged(object sender, PointerRoutedEventArgs e)
{
e.Handled = true;
PointerPoint pointerPoint = e.GetCurrentPoint(ListViewControl);
float scrolledDistance = pointerPoint.Properties.MouseWheelDelta;
if (scrolledDistance >=120)
{
//Load Page above current Page at a certain mousewheel point
}
else if (scrolledDistance <= -120 )
{
//Load Page below current Page at certain mousewheel point
}
else
{
//do some other stuff
}
}
The default ControlTemplate of ListView contains a ScrollViewer control. PointerWheelChanged is a relatively low-level event that will be intercepted by ScrollViewer.
If you want to monitor the change of the scrolling distance, PointerWheelChanged is not a recommended event. You can listen to the ScrollViewer.ViewChanged event and use ScrollViewer.VerticalOffset to determine the vertical scrolling distance.
We can create a custom ListView to achieve this:
CustomListView.cs
public class CustomListView:ListView
{
private ScrollViewer _scrollViewer;
public event EventHandler<ScrollViewerViewChangedEventArgs> ViewChanged;
public CustomListView()
{
this.DefaultStyleKey = typeof(ListView);
}
protected override void OnApplyTemplate()
{
_scrollViewer = GetTemplateChild("ScrollViewer") as ScrollViewer;
if (_scrollViewer != null)
{
_scrollViewer.ViewChanged += CustomViewChange;
}
base.OnApplyTemplate();
}
private void CustomViewChange(object sender, ScrollViewerViewChangedEventArgs e)
{
ViewChanged?.Invoke(sender, e);
}
}
Usage
<controls:CustomListView x:Name="ListViewControl"
ViewChanged="ListViewControl_ViewChanged"
...>
<!--other content-->
</controls:CustomListView>
private void ListViewControl_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
var scrollViewer = sender as ScrollViewer;
double scrollHeight = scrollViewer.VerticalOffset;
if (scrollHeight > 120)
{
//Do Something...
}
else
{
//Do something...
}
}
I want my content inside my GridView to scroll when I start my storyboard. Right now the storyboard targets the gridview and not the content. How do I make it to scroll inside the gridview?
This is how my Storyboards and DataTemplates look like:
<Page.Resources
<Storyboard x:Key="CarouselStoryboard">
<DoubleAnimation
Storyboard.TargetName="CarouselTransform"
Storyboard.TargetProperty="X"/>
</Storyboard>
<DataTemplate x:Key="CatTemplate">
<Grid>
<StackPanel Margin="30,0,30,0" Background="Red">
</StackPanel>
</Grid>
</DataTemplate>
<DataTemplate x:Key="DogTemplate">
<Grid>
<StackPanel Margin="30,0,30,0" Background="Green">
</StackPanel>
</Grid>
</DataTemplate>
</Page.Resources>
And this is how my Griview looks like:
<GridView x:Name="myGridView" ItemTemplateSelector="{StaticResource MyDataTemplateSelector}"
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
ScrollViewer.HorizontalScrollMode="Auto"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollMode="Disabled">
<GridView.RenderTransform>
<TranslateTransform x:Name="CarouselTransform" />
</GridView.RenderTransform>
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsWrapGrid Orientation="Vertical"/>
</ItemsPanelTemplate>
</GridView.ItemsPanel>
</GridView>
<Button Click="Left_Click" Content="Left" Background="Blue" HorizontalAlignment="Left"Width="405"/>
And this is how I start my storyboard using some C#
private int currentElement = 0;
private void Left_Click(object sender, RoutedEventArgs e)
{
if (currentElement < 100)
{
currentElement++;
AnimateCarousel();
}
}
private void AnimateCarousel()
{
Storyboard storyboard = (this.Resources["CarouselStoryboard"] as Storyboard);
DoubleAnimation animation = storyboard.Children.First() as DoubleAnimation;
animation.To = -200 * currentElement;
storyboard.Begin();
}
First of all, directly animate attach propery will not work due to we are lack of a reasonable built-in attached property:
What is scrollview in control template:
It's typical for a ScrollViewer control to exist as a composite part of other controls. A ScrollViewer part, along with the ScrollContentPresenter class for support, will display a viewport along with scrollbars only when the host control's layout space is being constrained smaller than the expanded content size. This is often the case for lists, so ListView and GridView templates always include a ScrollViewer.
To influence some of the behavior and properties that are from within the ScrollViewer part, ScrollViewer defines a number of XAML attached properties:
ScrollViewer.BringIntoViewOnFocusChange
ScrollViewer.HorizontalScrollBarVisibility
ScrollViewer.HorizontalScrollMode
ScrollViewer.IsDeferredScrollingEnabled
ScrollViewer.IsHorizontalRailEnabled
ScrollViewer.IsHorizontalScrollChainingEnabled
ScrollViewer.IsScrollInertiaEnabled
ScrollViewer.IsVerticalRailEnabled
ScrollViewer.IsVerticalScrollChainingEnabled
ScrollViewer.IsZoomChainingEnabled
ScrollViewer.IsZoomInertiaEnabled
ScrollViewer.VerticalScrollBarVisibility
ScrollViewer.VerticalScrollMode
ScrollViewer.ZoomMode
Animating XAML attached properties:We can only animate built-in attached properties.
So instead of the above idea we need to get the ScrollView embeded in GridView first:
Create your own GridView
public class MyGridView:GridView
{
private ScrollViewer myscrollviewer;
public ScrollViewer MyScrollViewer
{
get { return myscrollviewer; }
set { myscrollviewer = value; }
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
ScrollViewer testscrollviewer = GetTemplateChild("ScrollViewer") as ScrollViewer;
myscrollviewer = testscrollviewer;
}
}
Use it in your XAML:
<local:MyGridView x:Name="mygv".../>
Here since VerticalOffset is readonly, I cannot use DoubleAnimation to animate the scrollview. Thanks to Justin he has shared this workaround. With the help of ScrollView.ChangeView we can have some animation.
And a more simple way if you don't need the animation:
mygv.ScrollIntoView(mygv.Items[10], ScrollIntoViewAlignment.Leading);
I'm using the Items Page template from the Windows 8.1 Windows Store Apps templates in XAML. The page features a large GridView control with multiple item elements.
I would like to enable the dragging and reordering of items, but only after a user long clicks one of the items (similar to how it's done on the Windows Tablet Start menu and the iOS/Android home screen).
I've tried binding to the Holding event and enabling CanDragItems and CanReorderItems, but the user cannot start dragging the item during the Holding event.
Here's the GridView definition:
<GridView
x:Name="itemGridView"
AutomationProperties.AutomationId="ItemsGridView"
AutomationProperties.Name="Items"
TabIndex="1"
Grid.RowSpan="2"
Padding="116,136,116,46"
ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
SelectionMode="None"
IsSwipeEnabled="False"
IsItemClickEnabled="True"
CanReorderItems="False"
AllowDrop="False"
CanDragItems="False"
ItemClick="itemGridView_ItemClick"
>
With this in the code behind:
void OnHolding(object sender, HoldingRoutedEventArgs e)
{
if( e.HoldingState == Windows.UI.Input.HoldingState.Started)
{
Debug.WriteLine("Drag Start");
itemGridView.CanDragItems = true;
itemGridView.IsSwipeEnabled = true;
itemGridView.CanReorderItems = true;
itemGridView.AllowDrop = true;
}
else
{
Debug.WriteLine("Drag End");
itemGridView.CanDragItems = false;
itemGridView.IsSwipeEnabled = false;
itemGridView.CanReorderItems = false;
itemGridView.AllowDrop = false;
}
}
Thanks!
After much fuss and chasing down events, I was able to get the intended effect on pen, mouse, and touch devices.
The following code is not guaranteed to be the best way to accomplish the long click drag, but it is functioning on my devices with Windows 8.1. I encourage someone to find a better solution, since this one is kind of messy.
Code behind looks like this:
private bool isHolding = false;
private bool canUserDragItem = false;
// If a user moves their pointer outside the item's area or releases their pointer, stop all holding/dragging actions.
private void Grid_StopAllowDrag(object sender, PointerRoutedEventArgs e)
{
canUserDragItem = false;
isHolding = false;
}
// If a user starts dragging an item, check and see if they are holding the item first.
private void itemGridView_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
{
if (!canUserDragItem) e.Cancel = true;
}
private async void Grid_PointerPressed(object sender, PointerRoutedEventArgs e)
{
// Whenever a user presses the pointer inside the item, wait for half a second, then decide if the user is holding the item.
isHolding = true;
await Task.Delay(500); // Wait for some amount of time before allowing them to drag
if (isHolding) // If the user is still holding, allow them to drag the item.
{
canUserDragItem = true; // Allow them to drag now
// TODO: Make it apparent that the user is able to drag the item now.
}
}
And the XAML looks like this:
<GridView
x:Name="itemGridView"
AutomationProperties.AutomationId="ItemsGridView"
AutomationProperties.Name="Items"
TabIndex="1"
Grid.RowSpan="2"
Padding="116,136,116,46"
ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
SelectionMode="None"
IsSwipeEnabled="True" <!-- Enable dragging on touch devices -->
CanReorderItems="True" <!-- Allow users to try to start dragging -->
AllowDrop="True"
CanDragItems="True"
DragItemsStarting="itemGridView_DragItemsStarting" <!-- Stop dragging while not holding -->
>
<GridView.ItemTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Left" Width="250" Height="250"
<!-- Items must be given these event handlers -->
PointerPressed="Grid_PointerPressed"
PointerReleased="Grid_StopAllowDrag"
PointerCanceled="Grid_StopAllowDrag"
PointerCaptureLost="Grid_StopAllowDrag"
PointerExited="Grid_StopAllowDrag"
>
You need another else case. Your logic looks like this.
If they started doing something. Set properties to true.
While they are still holding the properties will be set to false.
You probably want two separate events. Holding started and Holding stopped. Not just a blanket else there.
I have a SelectionChanged event in a ListPicker within one of my application Pages that fires multiple times before the page is loaded. This is really inconvenient for me as when an item is selected, a MessageBox is displayed (and other actions will be performed). The MessageBox is displayed twice every time the page is NavigatedTo. How can I fix this?
XAML
<toolkit:ListPicker x:Name="ThemeListPicker" Header="Theme"
ItemTemplate="{StaticResource PickerItemTemplate}"
SelectionChanged="ThemeListPicker_SelectionChanged"/>
XAML.CS
private void ThemeListPicker_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
if(ThemeListPicker.SelectedIndex != -1)
{
var theme = (sender as ListPicker).SelectedItem;
if (index == 0)
{
Settings.LightTheme.Value = true;
MessageBox.Show("light");
}
else
{
Settings.LightTheme.Value = false;
MessageBox.Show("dark");
}
}
}
well, that's how a listpicker behaves, what best you can do is instead of making ThemeListPicker_SelectionChanged make a parent stackpanel inside the datatemplate somewhat like this
<Listpicker.ItemTemplate>
<DataTemplate x:Name="PickerItemTemplate">
<StackPanel tap="stk_Tap">
<TextBlock/>
</StackPanel>
</DataTemplate>
</Listpicker.ItemTemplate>
<Listpicker.FullModeItemTemplate>
<DataTemplate x:Name="PickerFullModeItemTemplate">
<StackPanel tap="stk_Tap">
<TextBlock/>
</StackPanel>
</DataTemplate>
<Listpicker.FullModeItemTemplate>
now use this tap stk_Tap to do your action as, this event would also get called every time the selection changed gets called but, it wont exhibit the buggy behavior like that of selection changed event.
hope this helps.
Attach the SelectionChanged event after the ListPicker is Loaded.
...
InitializeComponent();
YourListPicker.Loaded += YourListPicker_Loaded;
...
private void YourListPicker_Loaded(object sender, RoutedEventArgs e)
{
YourListPicker.SelectionChanged += YourListPicker_SelectionChanged;
}
I'm trying to create a tree header within a TreeView, but nothing will display. In my program, I'm creating a series of ellipse objects on a canvas that have names. What I'm trying to do is this:
When the ellipse object is created, create a header within the TreeView using the ellipse' names.
Since the first ellipse has a name of "Circle01," this is what I'm trying to display in the TreeView when the Left Mouse button is released. But when I do so, nothing happens.
// Ellipse object is created when Left Mouse button is release.
private void _canvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (mouseLBDown)
{
mouseLBDown = false;
if (isCreatingEllipse)
{
_debugLabel.Content = "Done.";
isCreatingEllipse = false;
loadToTree = true;
}
}
}
// Suppose to load name of Ellipse as a header, but doesn't.
private void _circleTree_Loaded(object sender, RoutedEventArgs e)
{
if (loadToTree)
{
TreeViewItem item = new TreeViewItem();
item.Header = circle[0].circleName;
_circleTree.Items.Add(item);
loadToTree = false;
}
}
XAML:
<TreeView Name="_boneTree" Background="#404040" Foreground="#E0E0E0"
HorizontalAlignment="Left" Height="422" Margin="970,28,0,0"
VerticalAlignment="Top" Width="212" Loaded="_circleTree_Loaded"/>
You should not create your own TreeViewItems, rather set the TreeView's ItemsSource and let WPF build the TreeViewItems for you. Also it is easier to do this in XAML:
<TreeView Name="_boneTree" Background="#404040" Foreground="#E0E0E0"
HorizontalAlignment="Left" Height="422" Margin="970,28,0,0"
VerticalAlignment="Top" Width="212"
ItemsSource="{Binding Circles}">
<TreeView.Resources>
<Style TargetType="TreeViewItem">
<Setter Property="Header" Value="{Binding circleName}"/>
</Style>
</TreeView.Resources>
</TreeView>
Where Circles is a property on your DataContext which by default will be your codebehind:
public ObservableCollection<Circle> Circles { get; set; }
Then whenever a Circle is added or removed it will automatically show up on the UI!