How to get ItemsControl scrollbar position programmatically? - wpf

I have an attached behavior to an ItemsControl that scrolls down to the bottom whenever a new item is added. Since I am working on a chat type program, I don't want it to scroll if the user has the scrollbar anywhere other than the very bottom as that would be very annoying otherwise(Some chat programs do this and it's awful).
How do I accomplish this? I don't know how to access the wrapping ScrollViewer, or otherwise figure out if I need to bring it into view or not.
This is the behavior class that I actually got from someone on StackOverflow. I'm still learning about behaviors myself.
public class ScrollOnNewItem : Behavior<ItemsControl>
{
protected override void OnAttached()
{
AssociatedObject.Loaded += OnLoaded;
AssociatedObject.Unloaded += OnUnLoaded;
}
protected override void OnDetaching()
{
AssociatedObject.Loaded -= OnLoaded;
AssociatedObject.Unloaded -= OnUnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;
if (incc == null) return;
incc.CollectionChanged += OnCollectionChanged;
}
private void OnUnLoaded(object sender, RoutedEventArgs e)
{
var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;
if (incc == null) return;
incc.CollectionChanged -= OnCollectionChanged;
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
int count = AssociatedObject.Items.Count;
if (count == 0)
return;
var item = AssociatedObject.Items[count - 1];
var frameworkElement = AssociatedObject.ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;
if (frameworkElement == null) return;
frameworkElement.BringIntoView();
}
}
}

Okay, here's the answer I came up with for myself.
I figured out that there is a GetSelfAndAncestors method for dependency objects. Using that, I am able to get the ScrollViewer ancestor(if there is one) of my AssociatedObject(the ItemsControl) and manipulate it with that.
So I added this field to my behavior
private ScrollViewer scrollViewer;
private bool isScrollDownEnabled;
And in the OnLoaded event handler I assigned it with the following code
scrollViewer = AssociatedObject.GetSelfAndAncestors().Where(a => a.GetType().Equals(typeof(ScrollViewer))).FirstOrDefault() as ScrollViewer;
And in the OnCollectionChanged event handler, I went ahead and wrapped all the logic in an if statement as follows
if (scrollViewer != null)
{
isScrollDownEnabled = scrollViewer.ScrollableHeight > 0 && scrollViewer.VerticalOffset + scrollViewer.ViewportHeight < scrollViewer.ExtentHeight;
if (e.Action == NotifyCollectionChangedAction.Add && !isScrollDownEnabled)
{
// Do stuff
}
}
So all together, the code looks like the following
public class ScrollOnNewItem : Behavior<ItemsControl>
{
private ScrollViewer scrollViewer;
private bool isScrollDownEnabled;
protected override void OnAttached()
{
AssociatedObject.Loaded += OnLoaded;
AssociatedObject.Unloaded += OnUnLoaded;
}
protected override void OnDetaching()
{
AssociatedObject.Loaded -= OnLoaded;
AssociatedObject.Unloaded -= OnUnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;
if (incc == null) return;
incc.CollectionChanged += OnCollectionChanged;
scrollViewer = AssociatedObject.GetSelfAndAncestors().Where(a => a.GetType().Equals(typeof(ScrollViewer))).FirstOrDefault() as ScrollViewer;
}
private void OnUnLoaded(object sender, RoutedEventArgs e)
{
var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;
if (incc == null) return;
incc.CollectionChanged -= OnCollectionChanged;
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (scrollViewer != null)
{
isScrollDownEnabled = scrollViewer.ScrollableHeight > 0 && scrollViewer.VerticalOffset + scrollViewer.ViewportHeight < scrollViewer.ExtentHeight;
if (e.Action == NotifyCollectionChangedAction.Add && !isScrollDownEnabled)
{
int count = AssociatedObject.Items.Count;
if (count == 0)
return;
var item = AssociatedObject.Items[count - 1];
var frameworkElement = AssociatedObject.ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;
if (frameworkElement == null) return;
frameworkElement.BringIntoView();
}
}
}
}
As asked in the comments, to use a behavior, I just need to add a new xmlns to my xaml file of the area in code that contains my behavior.
xmlns:behaviors="clr-namespace:Infrastructure.Behaviors;assembly=Infrastructure"
Then on the control I just add on the behavior.
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl Name="Blah" ItemsSource="{Binding Messages}" ItemTemplate="{StaticResource MessageTemplate}">
<i:Interaction.Behaviors>
<behaviors:ScrollOnNewItem />
</i:Interaction.Behaviors>
</ItemsControl>
</ScrollViewer>
The i class is just the interactivity namespace. xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

There is another way to implement this behavior. This way is easier than above. All you should do is invoke a method like below:
public void AppendText(RichTextBox richTextBox, string data){
richTextBox.AppendText(data);
bool isScrollDownEnabled = richTextBox.VerticalOffset == 0 ||
richTextBox.VerticalOffset + richTextBox.ViewportHeight == richTextBox.ExtentHeight;
if (isScrollDownEnabled)
richTextBox.ScrollToEnd();
}
It is suitable for TextBox too.

Related

WPF ItemsControl - how to know when the itemscontrol finished loading, so that I can focus the first one?

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;
}

Drag and drop multiple instances of user controls in WPF

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

WPF MVVM design question

In my View I have a TreeView with a event "TreeView_MouseLeftButtonDown". If it fires it proofs if the mouse clicked on a TreeViewItem. If not it deselects the last TreeViewItem.
My question is, should i do this in the code-behind or call a static methode in the viewmodel-class? How would you solve this?
The Methode:
private void treeView_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (sender != null)
{
var treeView = sender as TreeView;
if (treeView != null && treeView.SelectedItem != null)
TreeViewHelper.ReturnTreeViewItem(ref treeView, (XmlNode)treeView.SelectedItem).IsSelected = false;
}
}
XAML:
<TreeView ... KeyDown="TreeView_KeyDown"
MouseLeftButtonDown="TreeView_MouseLeftButtonDown"
SelectedItemChanged="TreeView_SelectedItemChanged" />
You are trying to add a behaviour to the TreeView.
The way I would implement this would be using Attached Properties. I would create an attached property called VerifiesLeftClick or similar and implement the logic in there. This way you do not need an event in the code behind.
See here for samples.
I made for you solution using attached behaviors which were pretty well described here Introduction to Attached Behaviors in WPF by Josh Smith
My solution:
public static class TreeViewBehavior
{
public static bool GetIsResetMouseLeftButtonDown(TreeView treeView)
{
return (bool)treeView.GetValue(IsResetMouseLeftButtonDownProperty);
}
public static void SetIsResetMouseLeftButtonDown(TreeView treeViewItem, bool value)
{
treeViewItem.SetValue(IsResetMouseLeftButtonDownProperty, value);
}
public static readonly DependencyProperty IsResetMouseLeftButtonDownProperty =
DependencyProperty.RegisterAttached("PreviewMouseLeftButtonDown", typeof(bool), typeof(TreeViewBehavior),
new UIPropertyMetadata(false, OnIsMouseLeftButtonDownChanged));
static void OnIsMouseLeftButtonDownChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
TreeView item = depObj as TreeView;
if (item == null)
return;
if (e.NewValue is bool == false)
return;
if ((bool)e.NewValue)
{
item.PreviewMouseLeftButtonDown += OnMouseLeftButtonDown;
}
else
{
item.MouseLeftButtonDown -= OnMouseLeftButtonDown;
}
}
static void OnMouseLeftButtonDown(object sender, RoutedEventArgs e)
{
var tempItem = e.Source as TreeViewItem;
if (tempItem != null && tempItem.IsSelected == false)
{
tempItem.IsSelected = true;
}
else
{
var tree = e.Source as TreeView;
if (tree != null && tree.SelectedItem != null)
{
var selItem = (tree.SelectedItem as TreeViewItem);
if (selItem != null)
{
selItem.IsSelected = false;
}
}
}
}
}
and then in View you should add this:
<TreeView local:TreeViewBehavior.IsResetMouseLeftButtonDown="True">
I hope my solution do what you are trying to achieve.

Setting focus to a control in WPF using MVVM

I want keyboard focus to be set to a TextBox when I click a Button on my view. I don't want to use any codebehind, so wondered if anyone had written an attached property or similar solution?
Try this out:
public static class FocusBehavior
{
public static readonly DependencyProperty ClickKeyboardFocusTargetProperty =
DependencyProperty.RegisterAttached("ClickKeyboardFocusTarget", typeof(IInputElement), typeof(FocusBehavior),
new PropertyMetadata(OnClickKeyboardFocusTargetChanged));
public static IInputElement GetClickKeyboardFocusTarget(DependencyObject obj)
{
return (IInputElement)obj.GetValue(ClickKeyboardFocusTargetProperty);
}
public static void SetClickKeyboardFocusTarget(DependencyObject obj, IInputElement value)
{
obj.SetValue(ClickKeyboardFocusTargetProperty, value);
}
private static void OnClickKeyboardFocusTargetChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var button = sender as ButtonBase;
if (button == null)
return;
if (e.OldValue == null && e.NewValue != null)
button.Click += OnButtonClick;
else if (e.OldValue != null && e.NewValue == null)
button.Click -= OnButtonClick;
}
private static void OnButtonClick(object sender, RoutedEventArgs e)
{
var target = GetKeyboardClickFocusTarget((ButtonBase)sender);
Keyboard.Focus(target);
}
}
Then to use it,
<TextBox x:Name="TargetTextBox"/>
<Button b:FocusBehavior.ClickKeyboardFocusTarget="{Binding ElementName=TargetTextBox}"/>

Interpret enter as tab WPF

I want to interpret Enter key as Tab key in whole my WPF application, that is, everywhere in my application when user press Enter I want to focus the next focusable control,except when button is focused. Is there any way to do that in application life circle? Can anyone give me an example?
Thanks a lot!
You can use my EnterKeyTraversal attached property code if you like. Add it to the top-level container on a WPF window and everything inside will treat enter as tab:
<StackPanel my:EnterKeyTraversal.IsEnabled="True">
...
</StackPanel>
Based on Richard Aguirre's answer, which is better than the selected answer for ease of use, imho, you can make this more generic by simply changing the Grid to a UIElement.
To change it in whole project you need to do this
In App.xaml.cs:
protected override void OnStartup(StartupEventArgs e)
{
EventManager.RegisterClassHandler(typeof(UIElement), UIElement.PreviewKeyDownEvent, new KeyEventHandler(Grid_PreviewKeyDown));
base.OnStartup(e);
}
private void Grid_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
var uie = e.OriginalSource as UIElement;
if (e.Key == Key.Enter)
{
e.Handled = true;
uie.MoveFocus(
new TraversalRequest(
FocusNavigationDirection.Next));
}
}
Compile.
And done it. Now you can use enter like tab.
Note: This work for elements in the grid
I got around woodyiii's issue by adding a FrameworkElement.Tag (whose value is IgnoreEnterKeyTraversal) to certain elements (buttons, comboboxes, or anything I want to ignore the enter key traversal) in my XAML. I then looked for this tag & value in the attached property. Like so:
if (e.Key == Key.Enter)
{
if (ue.Tag != null && ue.Tag.ToString() == "IgnoreEnterKeyTraversal")
{
//ignore
}
else
{
e.Handled = true;
ue.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
}
}
woodyiii, There is a function in the UIElement called PredictFocus() which by its name know its function, then you can check if that element is enabled or not so as to move the focus to it or not...
Here is Matt Hamilton's code, if anyone is wondering since his site is down apparently:
public class EnterKeyTraversal
{
public static bool GetIsEnabled(DependencyObject obj)
{
return (bool)obj.GetValue(IsEnabledProperty);
}
public static void SetIsEnabled(DependencyObject obj, bool value)
{
obj.SetValue(IsEnabledProperty, value);
}
static void ue_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
var ue = e.OriginalSource as FrameworkElement;
if (e.Key == Key.Enter)
{
e.Handled = true;
ue.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
}
}
private static void ue_Unloaded(object sender, RoutedEventArgs e)
{
var ue = sender as FrameworkElement;
if (ue == null) return;
ue.Unloaded -= ue_Unloaded;
ue.PreviewKeyDown -= ue_PreviewKeyDown;
}
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled", typeof(bool),
typeof(EnterKeyTraversal), new UIPropertyMetadata(false, IsEnabledChanged));
static void IsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var ue = d as FrameworkElement;
if (ue == null) return;
if ((bool)e.NewValue)
{
ue.Unloaded += ue_Unloaded;
ue.PreviewKeyDown += ue_PreviewKeyDown;
}
else
{
ue.PreviewKeyDown -= ue_PreviewKeyDown;
}
}
}
Another, a more on/off implementation approach would be to use behaviors:
public class TextBoxEnterFocusesNextBehavior :
Behavior<TextBox>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.PreviewKeyDown += AssociatedObjectOnPreviewKeyDown;
}
protected override void OnDetaching()
{
AssociatedObject.PreviewKeyDown -= AssociatedObjectOnPreviewKeyDown;
base.OnDetaching();
}
private void AssociatedObjectOnPreviewKeyDown(object sender, KeyEventArgs args)
{
if (args.Key != Key.Enter) { return; }
args.Handled = true;
AssociatedObject.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
}
}
Usage example:
<UserControl xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
xmlns:behaviors="clr-namespace:Your.Namespace.To.Behaviors"
...>
<DockPanel>
<TextBox x:Name="TextBoxWithBehavior"
DockPanel.Dock="Top">
<b:Interaction.Behaviors>
<behaviors:TextBoxEnterFocusesNextBehavior />
</b:Interaction.Behaviors>
</TextBox>
<TextBox x:Name="TextBoxWithoutBehavior"
DockPanel.Dock="Top" />
<TextBox x:Name="AnotherTextBoxWithBehavior"
DockPanel.Dock="Top">
<b:Interaction.Behaviors>
<behaviors:TextBoxEnterFocusesNextBehavior />
</b:Interaction.Behaviors>
</TextBox>
</DockPanel>
</UserControl>
My solution:
public class MoveToNext : TriggerAction<DependencyObject>
{
protected override void Invoke(object parameter)
{
if (parameter is RoutedEventArgs routedEventArgs && routedEventArgs.OriginalSource is FrameworkElement element)
{
routedEventArgs.Handled = true;
element.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
}
}
}
Usage:
<StackPanel>
<i:Interaction.Triggers>
<i:KeyTrigger Key="Return">
<util:MoveToNext/>
</i:KeyTrigger>
</i:Interaction.Triggers>
<!-- put your controls here -->
</StackPanel>
If you want the behavior to be attached to only one control instead of all controls within a layouter, simply add the <i:Interaction.Triggers block to that specific control.

Resources