In my WPF application I have a ListView whose ScrollViewer.VerticalScrollBarVisibility is set to Disabled. It is contained within a ScrollViewer. When I attempt to use the mouse wheel over the ListView, the outer ScrollViewer does not scroll because the ListView is capturing the scroll events.
How can I force the ListView to allow the scroll events to bubble up to the ScrollViewer?
You need to capture the preview mouse wheel event in the inner listview
MyListView.PreviewMouseWheel += HandlePreviewMouseWheel;
Or in the XAML
<ListView ... PreviewMouseWheel="HandlePreviewMouseWheel">
then stop the event from scrolling the listview and raise the event in the parent listview.
private void HandlePreviewMouseWheel(object sender, MouseWheelEventArgs e) {
if (!e.Handled) {
e.Handled = true;
var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
eventArg.RoutedEvent = UIElement.MouseWheelEvent;
eventArg.Source = sender;
var parent = ((Control)sender).Parent as UIElement;
parent.RaiseEvent(eventArg);
}
}
Creds go to #robert-wagner who solved this for me a few months ago.
Another nice solution using attached behavior.
I like it because it decoples the solution from the Control.
Create a no scroling behavior which will catch the PreviewMouseWheel(Tunneling) event and raise a new MouseWheelEvent(Bubbling)
public sealed class IgnoreMouseWheelBehavior : Behavior<UIElement>
{
protected override void OnAttached( )
{
base.OnAttached( );
AssociatedObject.PreviewMouseWheel += AssociatedObject_PreviewMouseWheel ;
}
protected override void OnDetaching( )
{
AssociatedObject.PreviewMouseWheel -= AssociatedObject_PreviewMouseWheel;
base.OnDetaching( );
}
void AssociatedObject_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
e.Handled = true;
var e2 = new MouseWheelEventArgs(e.MouseDevice,e.Timestamp,e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
AssociatedObject.RaiseEvent(e2);
}
}
Then attach the behavior to any UIElement with nested ScrollViewers case
<ListBox Name="ForwardScrolling">
<i:Interaction.Behaviors>
<local:IgnoreMouseWheelBehavior />
</i:Interaction.Behaviors>
</ListBox>
all credit to Josh Einstein Blog
If you're coming here looking for a solution to bubble the event ONLY if the child is at the top and scrolling up or the bottom and scrolling down, here's a solution. I only tested this with DataGrid, but it should work with other controls as well.
public class ScrollParentWhenAtMax : Behavior<FrameworkElement>
{
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.PreviewMouseWheel += PreviewMouseWheel;
}
protected override void OnDetaching()
{
this.AssociatedObject.PreviewMouseWheel -= PreviewMouseWheel;
base.OnDetaching();
}
private void PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
var scrollViewer = GetVisualChild<ScrollViewer>(this.AssociatedObject);
var scrollPos = scrollViewer.ContentVerticalOffset;
if ((scrollPos == scrollViewer.ScrollableHeight && e.Delta < 0)
|| (scrollPos == 0 && e.Delta > 0))
{
e.Handled = true;
var e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
AssociatedObject.RaiseEvent(e2);
}
}
private static T GetVisualChild<T>(DependencyObject parent) where T : Visual
{
T child = default(T);
int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numVisuals; i++)
{
Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null)
{
child = GetVisualChild<T>(v);
}
if (child != null)
{
break;
}
}
return child;
}
}
To attach this behavior, add the following XMLNS and XAML to your element:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
<i:Interaction.Behaviors>
<shared:ScrollParentWhenAtMax />
</i:Interaction.Behaviors>
There are different approaches depending on your exact situation, but I found this to work nicely. Assuming your basic situation is this:
<Window Height="200" Width="200">
<Grid>
<ScrollViewer Name="sViewer">
<StackPanel>
<Label Content="Scroll works here" Margin="10" />
<ListView Name="listTest" Margin="10"
PreviewMouseWheel="listTest_PreviewMouseWheel"
ScrollViewer.VerticalScrollBarVisibility="Disabled">
<ListView.ItemsSource>
<Int32Collection>
1,2,3,4,5,6,7,8,9,10
</Int32Collection>
</ListView.ItemsSource>
<ListView.View>
<GridView>
<GridViewColumn Header="Column 1" />
</GridView>
</ListView.View>
</ListView>
</StackPanel>
</ScrollViewer>
</Grid>
</Window>
Raising MouseWheelEvent yourself during PreviewMouseWheel seems to force the ScrollViewer to work. I wish I knew why, it seems very counterintuitive.
private void listTest_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
e.Handled = true;
MouseWheelEventArgs e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
listTest.RaiseEvent(e2);
}
You can also achieve the same thing using an attached behaviour. This has the advantage of not needing the System.Windows.Interactivity library. The logic has been taken from the other answers, only the implementation is different.
public static class IgnoreScrollBehaviour
{
public static readonly DependencyProperty IgnoreScrollProperty = DependencyProperty.RegisterAttached("IgnoreScroll", typeof(bool), typeof(IgnoreScrollBehaviour), new PropertyMetadata(OnIgnoreScollChanged));
public static void SetIgnoreScroll(DependencyObject o, string value)
{
o.SetValue(IgnoreScrollProperty, value);
}
public static string GetIgnoreScroll(DependencyObject o)
{
return (string)o.GetValue(IgnoreScrollProperty);
}
private static void OnIgnoreScollChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
bool ignoreScoll = (bool)e.NewValue;
UIElement element = d as UIElement;
if (element == null)
return;
if (ignoreScoll)
{
element.PreviewMouseWheel += Element_PreviewMouseWheel;
}
else
{
element.PreviewMouseWheel -= Element_PreviewMouseWheel;
}
}
private static void Element_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
UIElement element = sender as UIElement;
if (element != null)
{
e.Handled = true;
var e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
element.RaiseEvent(e2);
}
}
}
And then in the XAML:
<DataGrid ItemsSource="{Binding Items}">
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<ListView ItemsSource="{Binding Results}"
behaviours:IgnoreScrollBehaviour.IgnoreScroll="True">
<ListView.ItemTemplate>
<DataTemplate>
...
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
<DataGrid.Columns>
...
</DataGrid.Columns>
</DataGrid>
Thanks Keyle
I adapted your answer as an RX extension method
public static IDisposable ScrollsParent(this ItemsControl itemsControl)
{
return Observable.FromEventPattern<MouseWheelEventHandler, MouseWheelEventArgs>(
x => itemsControl.PreviewMouseWheel += x,
x => itemsControl.PreviewMouseWheel -= x)
.Subscribe(e =>
{
if(!e.EventArgs.Handled)
{
e.EventArgs.Handled = true;
var eventArg = new MouseWheelEventArgs(e.EventArgs.MouseDevice, e.EventArgs.Timestamp, e.EventArgs.Delta)
{
RoutedEvent = UIElement.MouseWheelEvent,
Source = e.Sender
};
var parent = ((Control)e.Sender).Parent as UIElement;
parent.RaiseEvent(eventArg);
}
});
}
Usage:
myList.ScrollsParent().DisposeWith(disposables);
My use case was slightly different. I have a very big scrollviewer and at the bottom another scrollviewer which has a maxheight of 600. I want to scroll the whole page to the bottom until I pass scrollevents to the inner scrollviewer.
This ensures you see the whole scrollviewer first, before you start scrolling.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Media;
namespace CleverScroller.Helper
{
public class ScrollParentWhenAtMax : Behavior<FrameworkElement>
{
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.PreviewMouseWheel += PreviewMouseWheel;
}
protected override void OnDetaching()
{
this.AssociatedObject.PreviewMouseWheel -= PreviewMouseWheel;
base.OnDetaching();
}
private void PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (e.Delta < 0)
{
var outerscroller = GetVisualParent<ScrollViewer>(this.AssociatedObject);
if (outerscroller.ContentVerticalOffset < outerscroller.ScrollableHeight)
{
e.Handled = true;
var e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
AssociatedObject.RaiseEvent(e2);
}
}
else
{
var scrollViewer = GetVisualChild<ScrollViewer>(this.AssociatedObject);
var scrollPos = scrollViewer.ContentVerticalOffset;
if ((scrollPos == scrollViewer.ScrollableHeight && e.Delta < 0)
|| (scrollPos == 0 && e.Delta > 0))
{
e.Handled = true;
var e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
AssociatedObject.RaiseEvent(e2);
}
}
}
private static T GetVisualChild<T>(DependencyObject parent) where T : Visual
{
T child = default(T);
int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numVisuals; i++)
{
Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null)
{
child = GetVisualChild<T>(v);
}
if (child != null)
{
break;
}
}
return child;
}
private static T GetVisualParent<T>(DependencyObject parent) where T : Visual
{
T obj = default(T);
Visual v = (Visual)VisualTreeHelper.GetParent(parent);
do
{
v = (Visual)VisualTreeHelper.GetParent(v);
obj = v as T;
} while (obj == null);
return obj;
}
}
}
Ok been a while since I have been on SO but I had to comment on this. Any Preview event tunnels, so why are we bubbling it up? Stop the tunnel in the parent and be done with it. in the parent add a PreviewMouseWheel event.
private void UIElement_OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
var scrollViewer = FindName("LeftPanelScrollViwer"); // name your parent mine is a scrollViewer
((ScrollViewer) scrollViewer)?.ScrollToVerticalOffset(e.Delta);
e.Handled = true;
}
Related
I want to Implement special button and i don't know even how to start with this.
I want my Button's content property to be: Play. When clicking on it, I want 2 other Buttons to pop up in the left and in the right sides: Single Play and Parallel Play
All you have to do is to create your 3 buttons and then put a visibility converter on your 2 sides buttons. Create a property that will hold if they should be visible or not and bind the visibility converter to this property. The Play button should modify this property when clicked.
I hope this gives you an idea on how to start with this.
After a lot of discussion, here is the result to solve this problem:
xaml:
<Window x:Class="WpfApplication3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="clr-namespace:WpfApplication3"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel Orientation="Horizontal">
<Button Name="btnSinglePlay" Visibility="Collapsed" my:VisibilityAnimation.IsActive="True">SinglePlay</Button>
<Button Name="btnPlay" Click="btnPlay_Click">Play</Button>
<Button Name="btnParallelPlay" Visibility="Collapsed" my:VisibilityAnimation.IsActive="True">ParallelPlay</Button>
</StackPanel>
</Grid>
C# to set the 2 sides button visible.
private void btnPlay_Click(object sender, RoutedEventArgs e)
{
btnSinglePlay.Visibility = Visibility.Visible;
btnParallelPlay.Visibility = Visibility.Visible;
}
And the c# code to permit the fade in/fade out. It comes from WPF Fade Animation so props to Anvaka, not to me.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Animation;
namespace WpfApplication3
{
public class VisibilityAnimation : DependencyObject
{
private const int DURATION_MS = 200;
private static readonly Hashtable _hookedElements = new Hashtable();
public static readonly DependencyProperty IsActiveProperty =
DependencyProperty.RegisterAttached("IsActive",
typeof(bool),
typeof(VisibilityAnimation),
new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnIsActivePropertyChanged)));
public static bool GetIsActive(UIElement element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (bool)element.GetValue(IsActiveProperty);
}
public static void SetIsActive(UIElement element, bool value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(IsActiveProperty, value);
}
static VisibilityAnimation()
{
UIElement.VisibilityProperty.AddOwner(typeof(FrameworkElement),
new FrameworkPropertyMetadata(Visibility.Visible, new PropertyChangedCallback(VisibilityChanged), CoerceVisibility));
}
private static void VisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// So what? Ignore.
}
private static void OnIsActivePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var fe = d as FrameworkElement;
if (fe == null)
{
return;
}
if (GetIsActive(fe))
{
HookVisibilityChanges(fe);
}
else
{
UnHookVisibilityChanges(fe);
}
}
private static void UnHookVisibilityChanges(FrameworkElement fe)
{
if (_hookedElements.Contains(fe))
{
_hookedElements.Remove(fe);
}
}
private static void HookVisibilityChanges(FrameworkElement fe)
{
_hookedElements.Add(fe, false);
}
private static object CoerceVisibility(DependencyObject d, object baseValue)
{
var fe = d as FrameworkElement;
if (fe == null)
{
return baseValue;
}
if (CheckAndUpdateAnimationStartedFlag(fe))
{
return baseValue;
}
// If we get here, it means we have to start fade in or fade out
// animation. In any case return value of this method will be
// Visibility.Visible.
var visibility = (Visibility)baseValue;
var da = new DoubleAnimation
{
Duration = new Duration(TimeSpan.FromMilliseconds(DURATION_MS))
};
da.Completed += (o, e) =>
{
// This will trigger value coercion again
// but CheckAndUpdateAnimationStartedFlag() function will reture true
// this time, and animation will not be triggered.
fe.Visibility = visibility;
// NB: Small problem here. This may and probably will brake
// binding to visibility property.
};
if (visibility == Visibility.Collapsed || visibility == Visibility.Hidden)
{
da.From = 1.0;
da.To = 0.0;
}
else
{
da.From = 0.0;
da.To = 1.0;
}
fe.BeginAnimation(UIElement.OpacityProperty, da);
return Visibility.Visible;
}
private static bool CheckAndUpdateAnimationStartedFlag(FrameworkElement fe)
{
var hookedElement = _hookedElements.Contains(fe);
if (!hookedElement)
{
return true; // don't need to animate unhooked elements.
}
var animationStarted = (bool)_hookedElements[fe];
_hookedElements[fe] = !animationStarted;
return animationStarted;
}
}
}
I've been trying to improve the behavior of the WPF ListBox control in the following way: The ListBox below automatically scrolls to the bottom as new items are added. It does this using the ScrollToBottom function shown. Using the preview events shown, if the user clicks an item, it stops scrolling, even if more items are added. (It would be obnoxious to let it keep scrolling!) If the user manually scrolls with the mouse or wheel, then it stops scrolling in the same way.
Right now I have a button in the code below that starts automatic scrolling again.
My question is this: How can I start off automatic scrolling if the user either scrolls the listbox all the way down to the bottom, or does the equivalent with the mouse wheel or keyboard. This is how my old Borland listboxes used to work out of the box.
using System;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
// Note requires .NET framework 4.5
namespace MMP
{
public partial class MainWindow : Window
{
public ObservableCollection<String> data { get; set; }
public MainWindow()
{
InitializeComponent();
data = new ObservableCollection<String>();
DataContext = this;
BeginAddingItems();
}
private async void BeginAddingItems()
{
await Task.Factory.StartNew(() =>
{
for (int i = 0; i < Int32.MaxValue; ++i)
{
if (i > 20)
Thread.Sleep(1000);
AddToList("Added " + i.ToString());
}
});
}
void AddToList(String item)
{
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new Action(() => { data.Add(item); ScrollToBottom(); }));
}
bool autoScroll = true;
public void ScrollToBottom()
{
if (!autoScroll)
return;
if (listbox.Items.Count > 0)
listbox.ScrollIntoView(listbox.Items[listbox.Items.Count - 1]);
}
private void listbox_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
autoScroll = false;
Console.WriteLine("PreviewMouseDown: setting autoScroll to false");
}
private void listbox_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
Console.WriteLine("PreviewMouseWheel: setting autoScroll to false");
autoScroll = false;
}
private void startButton_Click(object sender, RoutedEventArgs e)
{
ScrollToBottom(); // Catch up with the current last item.
Console.WriteLine("startButton_Click: setting autoScroll to true");
autoScroll = true;
}
private void listbox_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
// Can this be useful?
}
}
}
<Window x:Class="MMP.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Test Scrolling"
FontFamily="Verdana"
Width="400" Height="250"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox x:Name="listbox" Grid.Row="0"
PreviewMouseWheel="listbox_PreviewMouseWheel"
PreviewMouseDown="listbox_PreviewMouseDown"
ItemsSource="{Binding data}" ScrollViewer.ScrollChanged="listbox_ScrollChanged"
>
</ListBox>
<StackPanel Orientation="Horizontal" Grid.Row="1" HorizontalAlignment="Right">
<Button x:Name="startButton" Click="startButton_Click" MinWidth="80" >Auto Scroll</Button>
</StackPanel>
</Grid>
</Window>
The desired listbox behavior was achieved using the following code, with kind thanks to Roel for providing the initial Behavior<> framework above.
This is a sample project that contains the behavior code, along with a minimal WPF window that can be used to test the interactivity.
The test window contains a ListBox, to which items are added asynchronously via a background task. The important points of the behavior are as follows:
List box automatically scrolls to show new items as they are added asynchronously.
A user interaction with the listbox stops automatic scrolling - AKA obnoxious behavior.
Once finished interacting, to continue automatic scrolling, user drags the scroll bar to the bottom and lets go, or uses the mouse wheel or keyboard to do the same. This indicates that the user wants automatic scrolling to resume.
AutoScrolBehavior.cs:
using System;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
using System.Windows.Media;
namespace BehaviorTest.Code
{
// List box automatically scrolls to show new items as they are added asynchronously.
// A user interaction with the listbox stops automatic scrolling - AKA obnoxious behavior.
// Once finished interacting, to continue automatic scrolling, drag the scroll bar to
// the bottom and let go, or use the mouse wheel or keyboard to do the same.
// This indicates that the user wants automatic scrolling to resume.
public class AutoScrollBehavior : Behavior<ListBox>
{
private ScrollViewer scrollViewer;
private bool autoScroll = true;
private bool justWheeled = false;
private bool userInteracting = false;
protected override void OnAttached()
{
AssociatedObject.Loaded += AssociatedObjectOnLoaded;
AssociatedObject.Unloaded += AssociatedObjectOnUnloaded;
}
private void AssociatedObjectOnUnloaded(object sender, RoutedEventArgs routedEventArgs)
{
if (scrollViewer != null)
{
scrollViewer.ScrollChanged -= ScrollViewerOnScrollChanged;
}
AssociatedObject.SelectionChanged -= AssociatedObjectOnSelectionChanged;
AssociatedObject.ItemContainerGenerator.ItemsChanged -= ItemContainerGeneratorItemsChanged;
AssociatedObject.GotMouseCapture -= AssociatedObject_GotMouseCapture;
AssociatedObject.LostMouseCapture -= AssociatedObject_LostMouseCapture;
AssociatedObject.PreviewMouseWheel -= AssociatedObject_PreviewMouseWheel;
scrollViewer = null;
}
private void AssociatedObjectOnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
scrollViewer = GetScrollViewer(AssociatedObject);
if (scrollViewer != null)
{
scrollViewer.ScrollChanged += ScrollViewerOnScrollChanged;
AssociatedObject.SelectionChanged += AssociatedObjectOnSelectionChanged;
AssociatedObject.ItemContainerGenerator.ItemsChanged += ItemContainerGeneratorItemsChanged;
AssociatedObject.GotMouseCapture += AssociatedObject_GotMouseCapture;
AssociatedObject.LostMouseCapture += AssociatedObject_LostMouseCapture;
AssociatedObject.PreviewMouseWheel += AssociatedObject_PreviewMouseWheel;
}
}
private static ScrollViewer GetScrollViewer(DependencyObject root)
{
int childCount = VisualTreeHelper.GetChildrenCount(root);
for (int i = 0; i < childCount; ++i)
{
DependencyObject child = VisualTreeHelper.GetChild(root, i);
ScrollViewer sv = child as ScrollViewer;
if (sv != null)
return sv;
return GetScrollViewer(child);
}
return null;
}
void AssociatedObject_GotMouseCapture(object sender, System.Windows.Input.MouseEventArgs e)
{
// User is actively interacting with listbox. Do not allow automatic scrolling to interfere with user experience.
userInteracting = true;
autoScroll = false;
}
void AssociatedObject_LostMouseCapture(object sender, System.Windows.Input.MouseEventArgs e)
{
// User is done interacting with control.
userInteracting = false;
}
private void ScrollViewerOnScrollChanged(object sender, ScrollChangedEventArgs e)
{
// diff is exactly zero if the last item in the list is visible. This can occur because of scroll-bar drag, mouse-wheel, or keyboard event.
double diff = (scrollViewer.VerticalOffset - (scrollViewer.ExtentHeight - scrollViewer.ViewportHeight));
// User just wheeled; this event is called immediately afterwards.
if (justWheeled && diff != 0.0)
{
justWheeled = false;
autoScroll = false;
return;
}
if (diff == 0.0)
{
// then assume user has finished with interaction and has indicated through this action that scrolling should continue automatically.
autoScroll = true;
}
}
private void ItemContainerGeneratorItemsChanged(object sender, System.Windows.Controls.Primitives.ItemsChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add || e.Action == NotifyCollectionChangedAction.Reset)
{
// An item was added to the listbox, or listbox was cleared.
if (autoScroll && !userInteracting)
{
// If automatic scrolling is turned on, scroll to the bottom to bring new item into view.
// Do not do this if the user is actively interacting with the listbox.
scrollViewer.ScrollToBottom();
}
}
}
private void AssociatedObjectOnSelectionChanged(object sender, SelectionChangedEventArgs selectionChangedEventArgs)
{
// User selected (clicked) an item, or used the keyboard to select a different item.
// Turn off automatic scrolling.
autoScroll = false;
}
void AssociatedObject_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
{
// User wheeled the mouse.
// Cannot detect whether scroll viewer right at the bottom, because the scroll event has not occurred at this point.
// Same for bubbling event.
// Just indicated that the user mouse-wheeled, and that the scroll viewer should decide whether or not to stop autoscrolling.
justWheeled = true;
}
}
}
MainWindow.xaml.cs:
using BehaviorTest.Code;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Threading;
namespace BehaviorTest
{
public partial class MainWindow : Window
{
public ObservableCollection<String> data { get; set; }
public MainWindow()
{
InitializeComponent();
data = new ObservableCollection<String>();
DataContext = this;
Interaction.GetBehaviors(listbox).Add(new AutoScrollBehavior());
BeginAddingItems();
}
private async void BeginAddingItems()
{
List<Task> tasks = new List<Task>();
await Task.Factory.StartNew(() =>
{
for (int i = 0; i < Int32.MaxValue; ++i)
{
AddToList("Added Slowly: " + i.ToString());
Thread.Sleep(2000);
if (i % 3 == 0)
{
for (int j = 0; j < 5; ++j)
{
AddToList("Added Quickly: " + j.ToString());
Thread.Sleep(200);
}
}
}
});
}
void AddToList(String item)
{
if (Application.Current == null)
return; // Application is shutting down.
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new Action(() => { data.Add(item); }));
}
private void clearButton_Click(object sender, RoutedEventArgs e)
{
data.Clear();
}
private void listbox_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
MessageBox.Show("Launch a modal dialog. Items are still added to the list in the background.");
}
}
}
MainWindow.xaml.cs:
<Window x:Class="BehaviorTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Test Scrolling"
FontFamily="Verdana"
Width="400" Height="250"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox x:Name="listbox" Grid.Row="0"
ItemsSource="{Binding data}"
MouseDoubleClick="listbox_MouseDoubleClick" >
</ListBox>
<StackPanel Orientation="Horizontal" Grid.Row="1" HorizontalAlignment="Right">
<Button x:Name="startButton" Click="clearButton_Click" MinWidth="80" >Clear</Button>
</StackPanel>
</Grid>
</Window>
You could try creating a Blend Behavior that does this for you. This is a small start:
public class AutoScrollBehavior:Behavior<ListBox>
{
private ScrollViewer scrollViewer;
private bool autoScroll = true;
protected override void OnAttached()
{
AssociatedObject.Loaded += AssociatedObjectOnLoaded;
AssociatedObject.Unloaded += AssociatedObjectOnUnloaded;
}
private void AssociatedObjectOnUnloaded(object sender, RoutedEventArgs routedEventArgs)
{
AssociatedObject.SelectionChanged -= AssociatedObjectOnSelectionChanged;
AssociatedObject.ItemContainerGenerator.ItemsChanged -= ItemContainerGeneratorItemsChanged;
scrollViewer = null;
}
private void AssociatedObjectOnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
scrollViewer = GetScrollViewer(AssociatedObject);
if(scrollViewer != null)
{
scrollViewer.ScrollChanged += ScrollViewerOnScrollChanged;
AssociatedObject.SelectionChanged += AssociatedObjectOnSelectionChanged;
AssociatedObject.ItemContainerGenerator.ItemsChanged += ItemContainerGeneratorItemsChanged;
}
}
private void ScrollViewerOnScrollChanged(object sender, ScrollChangedEventArgs e) {
if (e.VerticalOffset == e.ExtentHeight-e.ViewportHeight) {
autoScroll = true;
}
}
private static ScrollViewer GetScrollViewer(DependencyObject root)
{
int childCount = VisualTreeHelper.GetChildrenCount(root);
for (int i = 0; i < childCount; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(root, i);
ScrollViewer sv = child as ScrollViewer;
if (sv != null)
return sv;
return GetScrollViewer(child);
}
return null;
}
private void ItemContainerGeneratorItemsChanged(object sender, System.Windows.Controls.Primitives.ItemsChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add || e.Action == NotifyCollectionChangedAction.Reset) {
if (autoScroll) {
scrollViewer.ScrollToBottom();
}
}
}
private void AssociatedObjectOnSelectionChanged(object sender, SelectionChangedEventArgs selectionChangedEventArgs)
{
autoScroll = false;
}
}
Have an ItemsControl in my View, that is bound to an ObservableCollection from ViewModel. The collection is filled, and afterwards an event from VM to view is raised (think search results and SearchFinished event).
how to move keyboard focus to the first item in an ItemsControl?
I'm using MVVM pattern
I've done this before using a behavior:
public sealed class FocusBehavior : Behavior<FrameworkElement>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += OnLoaded;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.Loaded -= OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
AssociatedObject.Focus();
}
}
This is then used in XAML as follows:
<TextBox x:Name="UsernameTextBox"
Text="{Binding Path=Username, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
KeyboardNavigation.TabIndex="0">
<i:Interaction.Behaviors>
<b:FocusBehavior />
</i:Interaction.Behaviors>
</TextBox>
You will need a reference to the System.Windows.Interactivity assembly to pull in the Interaction namespace.
I found the solution for this..
private void VerifiValue_Loaded(object sender, RoutedEventArgs e)
{
if (verificationIC.Items.Count > 0)
{
UIElement uiElement = (UIElement)verificationIC.ItemContainerGenerator.ContainerFromIndex(0);
TextBox t = GetChild<TextBox>(uiElement);
t.Focus();
t.SelectAll();
}
}
public T GetChild<T>(DependencyObject obj) where T : DependencyObject
{
DependencyObject child = null;
for (Int32 i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
child = VisualTreeHelper.GetChild(obj, i);
if (child != null && child.GetType() == typeof(T))
{
break;
}
else if (child != null)
{
child = GetChild<T>(child);
if (child != null && child.GetType() == typeof(T))
{
break;
}
}
}
return child as T;
}
The given code works fine with dragging and dropping one instance of control. If I try to drop the same instance again it throws an exception:
Specified element is already the logical child of another element. Disconnect it first.
How do I drop multiple instances of user controls on my Canvas, similar to how Visual Studio toolbox does?
public MainWindow()
{
InitializeComponent();
LoadUsercontrols();
}
private void LoadUsercontrols()
{
List<string> userControlKeys = new List<string>();
userControlKeys.Add("testCtrl1");
userControlKeys.Add("testCtrl2");
Type type = this.GetType();
Assembly assembly = type.Assembly;
foreach (string userControlKey in userControlKeys)
{
userControlFullName = String.Format("{0}.TestControls.{1}", type.Namespace, userControlKey);
UserControl userControl = new UserControl();
userControl = (UserControl)assembly.CreateInstance(userControlFullName);
_userControls.Add(userControlKey, userControl);
}
}
private void TreeViewItem_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
startPoint = e.GetPosition(null);
}
private void TreeViewItem_PreviewMouseMove(object sender, MouseEventArgs e)
{
// Get the current mouse position
System.Windows.Point mousePos = e.GetPosition(null);
Vector diff = startPoint - mousePos;
if (e.LeftButton == MouseButtonState.Pressed &&
Math.Abs(diff.X) > SystemParameters.MinimumHorizontalDragDistance &&
Math.Abs(diff.Y) > SystemParameters.MinimumVerticalDragDistance)
{
TreeView treeView = sender as TreeView;
TreeViewItem treeViewItem = FindAnchestor<TreeViewItem>((DependencyObject)e.OriginalSource);
if (treeViewItem != null)
{
Type type = this.GetType();
Assembly assembly = type.Assembly;
DataObject dragData = new DataObject("myFormat", _userControls[((System.Windows.Controls.HeaderedItemsControl)(treeViewItem)).Header.ToString()]);
DragDrop.DoDragDrop(treeViewItem, dragData, DragDropEffects.Copy);
}
}
}
private static T FindAnchestor<T>(DependencyObject current) where T : DependencyObject
{
do
{
if (current is T)
{
return (T)current;
}
current = VisualTreeHelper.GetParent(current);
}
while (current != null);
return null;
}
private void MyDesignerCanvas_DragEnter(object sender, DragEventArgs e)
{
if (!e.Data.GetDataPresent("myFormat") || sender == e.Source)
{
e.Effects = DragDropEffects.None;
}
}
private void MyDesignerCanvas_Drop(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent("myFormat"))
{
if (treeItem != null)
{
UserControl myCanvasItem = e.Data.GetData("myFormat") as UserControl;
UserControl newCanvastItem = new UserControl
{
Content = _userControls[((System.Windows.Controls.HeaderedItemsControl)(treeItem)).Header.ToString()]
};
Point position = e.GetPosition(MyDesignerCanvas);
DesignerCanvas.SetLeft(newCanvastItem, position.X);
DesignerCanvas.SetTop(newCanvastItem, position.Y);
DesignerCanvas.SetZIndex(newCanvastItem, 1);
MyDesignerCanvas.Children.Add(newCanvastItem);
}
}
}
In XAML Code:
<TreeView x:Name="presetTreeView4" Grid.Row="1" >
<TreeViewItem Header="testCtrl1" Selected="TreeViewItem_Selected" PreviewMouseLeftButtonDown="TreeViewItem_PreviewMouseLeftButtonDown" PreviewMouseMove="TreeViewItem_PreviewMouseMove"/>
<TreeViewItem Header="testCtrl2" Selected="TreeViewItem_Selected" PreviewMouseLeftButtonDown="TreeViewItem_PreviewMouseLeftButtonDown" PreviewMouseMove="TreeViewItem_PreviewMouseMove"/>
</TreeView>
<s:DesignerCanvas x:Name="MyDesignerCanvas" AllowDrop="True" Drop="MyDesignerCanvas_Drop" DragEnter="MyDesignerCanvas_DragEnter" Background="#A6B0D2F5" DockPanel.Dock="Bottom" Margin="0" >
</s:DesignerCanvas>
You cannot add the same control to different containers - a control can only appear once in the visual tree.
Instead of loading the user controls in advance, you should construct them at MyDesignerCanvas_Drop (i.e. use Activator the same way you're using it right now in LoadUsercontrols) and assign the resulting control to the UserControl.Content.
I think you have to clone control _userControls[((System.Windows.Controls.HeaderedItemsControl)(treeItem)).Header.ToString()] in MyDesignerCanvas_Drop
I know you can achieve this in Silverlight 4 by playing with the ListBoxItem style's LayoutStates, i.e. BeforeUnloaded, BeforeLoaded and AfterLoaded.
It doesn't seem to be working at all in WP7 although these states exist in the default style.
I am currently using version 7.1.
Is there any way I can get this working?
Thanks,
Xin
for this I used Artefact Animator, it's for Silverlight but works perfectly for WP7 also. The code shows only the addition. Whole code from the project's sample page.
MainPage.xaml
<UserControl.Resources>
<!-- ADDS SMOOTH SCROLL -->
<ItemsPanelTemplate x:Key="ItemsPanelTemplate">
<StackPanel/>
</ItemsPanelTemplate>
</UserControl.Resources>
<Grid>
<ListBox x:Name="lb" Height="247" Width="100" ItemsPanel="{StaticResource ItemsPanelTemplate}" />
<Button x:Name="addBtn" Content="Add" Height="72" HorizontalAlignment="Left" Margin="159,145,0,0" VerticalAlignment="Top" Width="160" />
</Grid>
MainPage.xaml.cs
public partial class MainPage : PhoneApplicationPage
{
private static ScrollViewer _scrollViewer;
// Constructor
public MainPage()
{
InitializeComponent();
Loaded += MainPage_Loaded;
}
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
// INIT
lb.Items.Clear();
lb.UpdateLayout();
// SCROLL INTERACTION
_scrollViewer = FindVisualChild<ScrollViewer>(lb);
var bar = FindVisualChild<ScrollBar>(_scrollViewer);
if (bar != null)
bar.ValueChanged += (s, args) => SetValue(ListBoxScrollOffsetProperty, args.NewValue);
// INPUT
addBtn.Click += (s, args) => AddItem();
}
private void AddItem()
{
// Create New ListBoxItem
var lbi = new ListBoxItem
{
Content = "Item " + lb.Items.Count,
RenderTransform = new CompositeTransform
{
TranslateX = -lb.Width
},
};
// Add ListBoxItem
lb.Items.Add(lbi);
lb.UpdateLayout();
// Animate In Item
ArtefactAnimator.AddEase(lbi.RenderTransform, CompositeTransform.TranslateXProperty, 0, 1, AnimationTransitions.CubicEaseOut, 0);
ArtefactAnimator.AddEase(this, ListBoxScrollOffsetProperty, _scrollViewer.ScrollableHeight, .8, AnimationTransitions.CubicEaseOut, 0);
}
// LISTBOX SCROLL OFFSET
public static readonly DependencyProperty ListBoxScrollOffsetProperty =
DependencyProperty.Register("ListBoxScrollOffset", typeof(double), typeof(MainPage), new PropertyMetadata(0.0, OnListBoxScrollOffsetChanged));
private static void OnListBoxScrollOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
_scrollViewer.ScrollToVerticalOffset((double)e.NewValue);
}
public double ListBoxScrollOffset
{
get
{
return (double)GetValue(ListBoxScrollOffsetProperty);
}
set
{
SetValue(ListBoxScrollOffsetProperty, value);
}
}
// VISUAL HELPER
public static childItem FindVisualChild<childItem>(DependencyObject obj) where childItem : DependencyObject
{
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
var child = VisualTreeHelper.GetChild(obj, i);
if (child != null && child is childItem)
{
return (childItem)child;
}
else
{
var childOfChild = FindVisualChild<childItem>(child);
if (childOfChild != null)
{
return childOfChild;
}
}
}
return null;
}
}