TextBox ScrollToEnd Glitches When Selecting if Text is Appeneded - c#

I'm making a textbox to autoscroll to the end when text is added. However I wanted the option to not scroll the textbox when the mouse is over the textbox. I've done all that, but when the user selects text and the textbox receives an event to update the text, everything goes haywire.
Here's what I'm working with:
<TextBox Text="{Binding ConsoleContents, Mode=OneWay}" TextWrapping="Wrap"
IsReadOnly="True" ScrollViewer.VerticalScrollBarVisibility="Visible"
TextChanged="TextBox_TextChanged" MouseEnter="TextBox_MouseEnterLeave"
MouseLeave="TextBox_MouseEnterLeave" AllowDrop="False" Focusable="True"
IsUndoEnabled="False"></TextBox>
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox textBox = sender as TextBox;
if (textBox == null) return;
// ensure we can scroll
if (_canScroll)
{
textBox.Select(textBox.Text.Length, 0); //This was an attempt to fix the issue
textBox.ScrollToEnd();
}
}
private void TextBox_MouseEnterLeave(object sender, MouseEventArgs e)
{
TextBox textBox = sender as TextBox;
// Don't scroll if the mouse is in the box
if (e.RoutedEvent.Name == "MouseEnter")
{
_canScroll = false;
}
else if (e.RoutedEvent.Name == "MouseLeave")
{
_canScroll = true;
}
}
To further explain what haywire means, when the textbox receives and propertychanged event it sets the text and scrolls down to the end if the mouse is not hovering over it. If the mouse is hovering over it does not scroll. But if I select text and the textbox recives the propertychanged event the content gets updated, but the box does not scroll down. This is expected. The problem is that my selection then goes from where the cursor currently is to the top of the text. If I remove the cursor from the box, it continues fine but once the cursor returns, the box gets stuck at the top and cannot scroll down. I thought it might have been the cursor so I try to move it to the end but that solves nothing.
Any ideas?!
I've been ripping my hair out! Thanks!

You will need to handle the extra case of PropertyChanged event. Because the text is changed the control is updated. Because it updates it resets certain values, like cursor location and selected text and such.
You can try to temporarily save these settings like CaretIndex, SelectionStart and SelectionLength. Although I have no experience with this, so you will have to find out for yourself what values you want to keep.
You could also apply the same _canScroll check when the PropertyChanged event is triggered for the TextBox. This allows you to work with the text. But if the mouse leaves the Textbox you must wait for a new event before the latest text is shown.
You might also want to look into using IsFocused property. In my opinion it gives a nicer solution than the MouseEnter and MouseLeave event. But that's up to you of course.

Well it took all morning but here's how to do it:
<TextBox Text="{Binding ConsoleContents, Mode=OneWay}"
TextWrapping="Wrap" IsReadOnly="True" ScrollViewer.VerticalScrollBarVisibility="Visible" TextChanged="TextBox_TextChanged"
MouseEnter="TextBox_MouseEnterLeave" MouseLeave="TextBox_MouseEnterLeave" SelectionChanged="TextBox_SelectionChanged" AllowDrop="False" Focusable="True"
IsUndoEnabled="False"></TextBox>
public partial class ConsoleView : UserControl
{
private bool _canScroll;
// saves
private int _selectionStart;
private int _selectionLength;
private string _selectedText;
public ConsoleView(ConsoleViewModel vm)
{
InitializeComponent();
this.DataContext = vm;
_canScroll = true;
}
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox textBox = sender as TextBox;
if (textBox == null) return;
// ensure we can scroll
if (_canScroll)
{
// set the cursor to the end and scroll down
textBox.Select(textBox.Text.Length, 0);
textBox.ScrollToEnd();
// save these so the box doesn't jump around if the user goes back in
_selectionLength = textBox.SelectionLength;
_selectionStart = textBox.SelectionStart;
}
else if (!_canScroll)
{
// move the cursor to where the mouse is if we're not selecting anything (for if we are selecting something the cursor has already moved to where it needs to be)
if (string.IsNullOrEmpty(_selectedText))
//if (textBox.SelectionLength > 0)
{
textBox.CaretIndex = textBox.GetCharacterIndexFromPoint(Mouse.GetPosition(textBox), true);
}
else
{
textBox.Select(_selectionStart, _selectionLength); // restore what was saved
}
}
}
private void TextBox_MouseEnterLeave(object sender, MouseEventArgs e)
{
TextBox textBox = sender as TextBox;
if (textBox == null) return;
// Don't scroll if the mouse is in the box
if (e.RoutedEvent.Name == "MouseEnter")
{
_canScroll = false;
}
else if (e.RoutedEvent.Name == "MouseLeave")
{
_canScroll = true;
}
}
private void TextBox_SelectionChanged(object sender, RoutedEventArgs e)
{
TextBox textBox = sender as TextBox;
if (textBox == null) return;
// save all of the things
_selectionLength = textBox.SelectionLength;
_selectionStart = textBox.SelectionStart;
_selectedText = textBox.SelectedText; // save the selected text because it gets destroyed on text update before the TexChanged event.
}
}

Related

user control loses keyboard focus

I made a user control, which I called InputTextBox, that present text which the user can edit only after clicking the control:
<Grid>
<TextBox Name="box"
Text="{Binding RelativeSource={RelativeSource AncestorType=local:InputTextBlock}, Path=Text, Mode=TwoWay}"
Visibility="Hidden"
LostKeyboardFocus="box_LostKeyboardFocus"
KeyDown="box_KeyDown"/>
<Button Name="block"
Background="Transparent"
BorderThickness="0"
Click="block_Click">
<Button.Content>
<TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType=local:InputTextBlock}, Path=Text}" />
</Button.Content>
</Button>
</Grid>
When the user click the button the following callack is used:
private void block_Click(object sender, RoutedEventArgs e)
{
StartEdit();
}
public void StartEdit()
{
box.Visibility = Visibility.Visible;
block.Visibility = Visibility.Hidden;
box.CaretIndex = box.Text.Length;
Keyboard.Focus(box);
}
There are two more important events that are handled within the control. The first is when the control loses keyboard focus:
private void box_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
box.Visibility = Visibility.Hidden;
block.Visibility = Visibility.Visible;
}
And the other is when the user presses the TAB or ENTER keys:
private void box_KeyDown(object sender, KeyEventArgs e)
{
Key key = e.Key;
if (key == Key.Enter || key == Key.Tab)
{
RoutedEventArgs args = new RoutedEventArgs(KeyExitEvent, this, e.Key);
RaiseEvent(args);
}
}
This raises a simple routed event I registered for this control called KeyExit.
So, basically, it's like this control has two "modes": "edit mode" which the user can activate by simply clicking on the control, and "view mode", which the user can return to by giving any other control in the UI keyboard focus.
In my UI I have a stack panel with a bunch of these controls in it - each one is wrapped inside a class I created that is similar in concept to ListViewItem. The idea is that when the user is in edit mode inside one of the items in the stack panel and click the TAB or ENTER key, the next control in the panel will get into edit mode.
So, I have the following event callback:
private void item_KeyExit(object sender, RoutedEventArgs e)
{
FrameworkElement obj = e.OriginalSource as FrameworkElement;
if (obj != null)
{
var listItem = VisualTreeHelperUtils.FindFirstAncestorOfType<MyListItem>(obj);
if (listItem != null)
{
int itemIndex = stackPanelList.Children.IndexOf(listItem);
MyListItem nextItem = null;
if (itemIndex == ucSortableList.Children.Count - 1)
{
nextItem = stackPanelList.Children[itemIndex+1] as MyListItem;
}
if (nextItem != null)
{
var item = nextItem.DataContent; // property I made in MyListItem that gives access to the class it wraps
InputTextBlock block = item as InputTextBlock;
if (block != null)
{
block.StartEdit();
}
}
}
}
}
Everything is called properly, but immediately after all of it is done, the previous item, which I tabbed out of, gets the keyboard focus back and cause no item in the stack to be in edit mode. So, if, for example, the first item in the stack was in edit mode, once the user hit the tab key, the second item in the stack gets into edit mode, but then the first item gets keyboard focus back immediately. Can anybody understand why this is happening?
StackPanel Focusable is false by default. Suggest ensure it's true.
Also for checking indeces, one generally wants the index to be < count. So your index + 1 could == count if it's == count - 1. Don't you want:
if (itemIndex < ...Count - 1)
That way, item index + 1 will always be <= Count - 1 ?
if (itemIndex == ucSortableList.Children.Count - 1)
{
nextItem = stackPanelList.Children[itemIndex+1] as MyListItem;
}
Also you can use box.Focus() instead of Keyboard.Focus(box) but that may not be necessary.

Popup with StaysOpen to false is not closing

I have a telerik grid view and when I right click the header, I'm showing a with a ListBox inside containing the list of columns.
The item template is redefined to show a check box so I can set the column visible or not.
I can also drag/drop columns to reorder them.
So here is how I create my Popup:
var view = new ColumnsOrderer.ColumnsOrderer
{
DataContext = new ColumnsOrderer.ViewModelColumnsOrderer(Columns)
};
var codePopup = new Popup
{
Child = view,
MaxHeight = 400,
StaysOpen = false,
Placement = PlacementMode.Mouse
};
codePopup.IsOpen = true;
Now everything seems to work correctly, but it's not.
If I set columns visible or hidden and then click outside the popup, it closes correctly.
Though if I drag an item to reorder it, the popup seems to lose focus and then it won't close if I click outside the popup. I have to click back in the list box inside the popup and then it closes by clicking outside.
Here is my drag/drop events:
public ColumnsOrderer()
{
InitializeComponent();
InitialiazeListBoxDragDrop();
}
private void InitialiazeListBoxDragDrop()
{
var itemContainerStyle = new Style(typeof(ListBoxItem));
itemContainerStyle.Setters.Add(new Setter(AllowDropProperty, true));
itemContainerStyle.Setters.Add(new EventSetter(PreviewMouseMoveEvent, new MouseEventHandler(OnMouseMove)));
itemContainerStyle.Setters.Add(new EventSetter(DropEvent, new DragEventHandler(OnDrop)));
listColumns.ItemContainerStyle = itemContainerStyle;
}
void OnMouseMove(object sender, MouseEventArgs e)
{
if (e.OriginalSource is CheckBox || e.LeftButton == MouseButtonState.Released)
return;
if (sender is ListBoxItem)
{
var draggedItem = sender as ListBoxItem;
draggedItem.IsSelected = true;
DragDrop.DoDragDrop(draggedItem, draggedItem.DataContext, DragDropEffects.Move);
}
}
void OnDrop(object sender, DragEventArgs e)
{
if (!(sender is ListBoxItem))
return;
}
An interesting thing is that if I remove the OnDrop handler, the problem is not there.
I tried many ways to set back the focus to the popup, but it's not working.
Could anyone help me on that?
How about trying to re-focus your Popup control after the drag and drop operation?
void OnDrop(object sender, DragEventArgs e)
{
if (!(sender is ListBoxItem))
return;
codePopup.Focus();
}

C# Display a tooltip on disabled textbox (Form)

I am trying to get a tooltip to display on a disabled textbox during a mouse over. I know because the control is disabled the following won't work:
private void textBox5_MouseHover(object sender, EventArgs e)
{
// My tooltip display code here
}
How can I get the tooltip to display on a mouse over of a disabled control?
Many thanks
MouseHover wont fire if control is disabled. Instead you can check in Form MouseMove event whether you hover the textbox
public Form1()
{
InitializeComponent();
textBox1.Enabled = false;
toolTip.InitialDelay = 0;
}
private ToolTip toolTip = new ToolTip();
private bool isShown = false;
private void Form1_MouseMove(object sender, MouseEventArgs e)
{
if(textBox1 == this.GetChildAtPoint(e.Location))
{
if(!isShown)
{
toolTip.Show("MyToolTip", this, e.Location);
isShown = true;
}
}
else
{
toolTip.Hide(textBox1);
isShown = false;
}
}
Late to the party, but had the same problem and found a better solution: you can just wrap your TextBox in another Item and put a ToolTip on it like:
<Grid ToolTip="ToolTip to display">
<TextBox IsEnabled="False" Text="Text to display" />
</Grid>
You can also drag a ToolTip object from the Toolbox in designer onto the form.
Then in the code you just call SetToolTip() and pass in the button or text box etc. you want the tool tip to assign to and the text you want it to show.
myToolTip.SetToolTip(myTextBox, "You just hovered over myTextBox!");

Detect when WPF listview scrollbar is at the bottom?

Is there a way to detect if the scrollbar from the ScrollViewer in a ListView has reached the bottom of the virtual scroll space? I would like to detect this to fetch more items from the server to put into the bound ObservableCollection on the ListView.
Right now I'm doing this:
private void currentTagNotContactsList_scrollChanged(object sender, ScrollChangedEventArgs e) {
ListView v = (ListView)sender;
if (e.VerticalOffset + e.ViewportHeight == e.ExtentHeight) {
Debug.Print("At the bottom of the list!");
}
}
Is this even correct? I also need to differentiate between the vertical scrollbar causing the event and the horizontal scrollbar causing it (i.e. I don't want to keep generating calls to the server if you scroll horizontally at the bottom of the box).
Thanks.
//A small change in the "Max's" answer to stop the repeatedly call.
//this line to stop the repeatedly call
ScrollViewer.CanContentScroll="False"
private void dtGrid_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
//this is for vertical check & will avoid the call at the load time (first time)
if (e.VerticalChange > 0)
{
if (e.VerticalOffset + e.ViewportHeight == e.ExtentHeight)
{
// Do your Stuff
}
}
}
I figured it out. It seems I should have been getting events from the ScrollBar (<ListView ScrollBar.Scroll="currentTagNotContactsList_Scroll" in XAML) itself, rather than the viewer. This works, but I just have to figure a way to avoid the event handler being called repeatedly once the scrollbar is down. Maybe a timer would be good:
private void currentTagNotContactsList_Scroll(object sender, ScrollEventArgs e) {
ScrollBar sb = e.OriginalSource as ScrollBar;
if (sb.Orientation == Orientation.Horizontal)
return;
if (sb.Value == sb.Maximum) {
Debug.Print("At the bottom of the list!");
}
}
For UWP I got it like this
<ScrollViewer Name="scroll" ViewChanged="scroll_ViewChanged">
<ListView />
</ScrollViewer>
private void scroll_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
var scrollViewer = (ScrollViewer)sender;
if (scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight)
btnNewUpdates.Visibility = Visibility.Visible;
}
you can try this way:
<ListView ScrollViewer.ScrollChanged="Scroll_ScrollChanged">
and in Back:
private void Scroll_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
// Get the border of the listview (first child of a listview)
Decorator border = VisualTreeHelper.GetChild(sender as ListView, 0) as Decorator;
// Get scrollviewer
ScrollViewer scrollViewer = border.Child as ScrollViewer;
if (scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight)
Debug.Print("At the bottom of the list!");
}
Not recommand to use ScrollBar.Scroll , beacause if you scroll the middle wheel of the mouse, it won't work.
ScrollBar.Scroll="currentTagNotContactsList_Scroll"
The following support both right side scroll bar and mouse's wheel scroll.
in listbox's xmal:
ScrollViewer.ScrollChanged="ScrollViewer_ScrollChanged"
in c#:
private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
var listBox = (ListBox)sender;
var scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(listBox, 0);
if (scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight)
{
Console.WriteLine("____At the bottom of the list!");
}
}

WPF: Dropdown of a Combobox highlightes the text

When I type in the combobox I automatically opens enables the dropdown list
searchComboBox.IsDropDownOpen = true;
The problem here is - the text gets highlighted and the next keystrock overwrites the previous text.
How can I disable the text highlighting when ComboBox DropDown opens up?
I had this very same issue and like some of the users being new to WPF, struggled to get the solution given by Einar Guðsteinsson to work. So in support of his answer I'm pasting here the steps to get this to work. (Or more accurately how I got this to work).
First create a custom combobox class which inherits from the Combobox class. See code below for full implementation. You can change the code in OnDropSelectionChanged to suit your individual requirements.
namespace MyCustomComboBoxApp
{
using System.Windows.Controls;
public class MyCustomComboBox : ComboBox
{
private int caretPosition;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var element = GetTemplateChild("PART_EditableTextBox");
if (element != null)
{
var textBox = (TextBox)element;
textBox.SelectionChanged += OnDropSelectionChanged;
}
}
private void OnDropSelectionChanged(object sender, System.Windows.RoutedEventArgs e)
{
TextBox txt = (TextBox)sender;
if (base.IsDropDownOpen && txt.SelectionLength > 0)
{
caretPosition = txt.SelectionLength; // caretPosition must be set to TextBox's SelectionLength
txt.CaretIndex = caretPosition;
}
if (txt.SelectionLength == 0 && txt.CaretIndex != 0)
{
caretPosition = txt.CaretIndex;
}
}
}
Ensure that this custom combo class exists in the same project. THen you can use the code below to reference this combo in your UI.
<Window x:Class="MyCustomComboBoxApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="clr-namespace:MyCustomComboBoxApp"
Title="MainWindow" Height="350" Width="525" FocusManager.FocusedElement="{Binding ElementName=cb}">
<Grid>
<StackPanel Orientation="Vertical">
<cc:MyCustomComboBox x:Name="cb" IsEditable="True" Height="20" Margin="10" IsTextSearchEnabled="False" KeyUp="cb_KeyUp">
<ComboBoxItem>Toyota</ComboBoxItem>
<ComboBoxItem>Honda</ComboBoxItem>
<ComboBoxItem>Suzuki</ComboBoxItem>
<ComboBoxItem>Vauxhall</ComboBoxItem>
</cc:MyCustomComboBox>
</StackPanel>
</Grid>
</Window>
Thats it! Any questions, please ask! I'll do my best to help.
THanks to Einar Guðsteinsson for his solution!
Better late than never and if some one else hit this proplem he might use this.
There is away todo this if you override combobox.
First get handle on the textbox that is used in the template and register to selectionchanged event.
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var element = GetTemplateChild("PART_EditableTextBox");
if (element != null)
{
var textBox = (TextBox)element;
textBox.SelectionChanged += OnDropSelectionChanged;
}
}
private void OnDropSelectionChanged(object sender, RoutedEventArgs e)
{
// Your code
}
Then in the event handler you can set the selection again like you want it to be. In my case I was calling IsDropDownOpen in code. Saved selection there then put it back in the event handler. Ugly but did the trick.
Further to clsturgeon's answer, I have solved the issue by setting the selection when DropDownOpened event occurred:
private void ItemCBox_DropDownOpened(object sender, EventArgs e)
{
TextBox textBox = (TextBox)((ComboBox)sender).Template.FindName("PART_EditableTextBox", (ComboBox)sender);
textBox.SelectionStart = ((ComboBox)sender).Text.Length;
textBox.SelectionLength = 0;
}
I think in the Solution provided by Andrew N there is something missing as when I tried it out the Selection Changed event of the TextBox was placing the caret at the wrong place. So I made this change to solve that.
namespace MyCustomComboBoxApp { using System.Windows.Controls;
public class MyCustomComboBox : ComboBox
{
private int caretPosition;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var element = GetTemplateChild("PART_EditableTextBox");
if (element != null)
{
var textBox = (TextBox)element;
textBox.SelectionChanged += OnDropSelectionChanged;
}
}
private void OnDropSelectionChanged(object sender, System.Windows.RoutedEventArgs e)
{
TextBox txt = (TextBox)sender;
if (base.IsDropDownOpen && txt.SelectionLength > 0)
{
caretPosition = txt.SelectionLength; // caretPosition must be set to TextBox's SelectionLength
txt.CaretIndex = caretPosition;
}
if (txt.SelectionLength == 0 && txt.CaretIndex != 0)
{
caretPosition = txt.CaretIndex;
}
}
}
When a comboxbox gains focus you can disable the text highlighting (i.e. by selecting no text upon the GotFocus event). However, when you pulldown the combobox the system is going to locate the item in the list and make that the selected item. This in turn automatically highlights the text. If I understand the behaviour you are looking for, I do not believe it is fully possible.
I was able to fix it using a modified answer from Jun Xie. Assuming you are using the keyUp event for your combobox search, I found an edge case in my custom use case that would still overwrite the text:
Type in the combobox for the first time. Text is fine.
Use up and down arrow keys to select an item in the list, but not "committing" the change (pressing enter, for example, and closing the dropDown selections. Note the text is highlighted at this point like clsturgeon points out.
Try to type in the textbox again. In this case the text will be over-ridden because the dropdown was still open hence the event never fired to clear the highlight.
The solution is to check if an item is selected. Here's the working code:
XAML:
<ComboBox x:Name="SearchBox" IsEditable="True" KeyUp="SearchBox_KeyUp"
PreviewMouseDown="SearchBox_PreviewMouseDown" IsTextSearchEnabled="False"
DropDownOpened="SearchBox_DropDownOpened">
</ComboBox>
Code:
private void SearchBox_KeyUp(object sender, KeyEventArgs e)
{
SearchBox.IsDropDownOpen = true;
if (e.Key == Key.Down || e.Key == Key.Up)
{
e.Handled = true;
//if trying to navigate but there's noting selected, then select one
if(SearchBox.Items.Count > 0 && SearchBox.SelectedIndex == -1)
{
SearchBox.SelectedIndex = 0;
}
}
else if (e.Key == Key.Enter)
{
//commit to selection
}
else if (string.IsNullOrWhiteSpace(SearchBox.Text))
{
SearchBox.Items.Clear();
SearchBox.IsDropDownOpen = false;
SearchBox.SelectedIndex = -1;
}
else if (SearchBox.Text.Length > 1)
{
//if something is currently selected, then changing the selected index later will loose
//focus on textbox part of combobox and cause the text to
//highlight in the middle of typing. this will "eat" the first letter or two of the user's search
if(SearchBox.SelectedIndex != -1)
{
TextBox textBox = (TextBox)((ComboBox)sender).Template.FindName("PART_EditableTextBox", (ComboBox)sender);
//backup what the user was typing
string temp = SearchBox.Text;
//set the selected index to nothing. sets focus to dropdown
SearchBox.SelectedIndex = -1;
//restore the text. sets focus and highlights the combobox text
SearchBox.Text = temp;
//set the selection to the end (remove selection)
textBox.SelectionStart = ((ComboBox)sender).Text.Length;
textBox.SelectionLength = 0;
}
//your search logic
}
}
private void SearchBox_DropDownOpened(object sender, EventArgs e)
{
TextBox textBox = (TextBox)((ComboBox)sender).Template.FindName("PART_EditableTextBox", (ComboBox)sender);
textBox.SelectionStart = ((ComboBox)sender).Text.Length;
textBox.SelectionLength = 0;
}
An alternative. Prevent the framework from messing with selection.
public class ReflectionPreventSelectAllOnDropDown
{
private static readonly PropertyInfo edtbPropertyInfo;
static ReflectionPreventSelectAllOnDropDown()
{
edtbPropertyInfo = typeof(ComboBox).GetProperty("EditableTextBoxSite", BindingFlags.NonPublic | BindingFlags.Instance);
}
public void DropDown(ComboBox comboBox)
{
if (!comboBox.IsDropDownOpen)
{
var edtb = edtbPropertyInfo.GetValue(comboBox);
edtbPropertyInfo.SetValue(comboBox, null);
comboBox.IsDropDownOpen = true;
edtbPropertyInfo.SetValue(comboBox, edtb);
}
}
}

Categories