I have a problem with the telerik Combobox which can be also reproduced for the basic WPF Combobox.
I have the following scenario : when the user tries to open the combobox, the entire page (including this control) should be shifted up, because in the bottom section the space will be occupied by a keyboard.
In order to do this I have catched the GotFocus event and I have updated the position of the Scrollviewer programmatically.
Here is the XAML :
<ScrollViewer Grid.Row="2" x:Name="KeyBoardScrollViewer" PanningMode="VerticalOnly" VerticalScrollBarVisibility="Hidden">
<telerik:RadComboBox Grid.Row="5" GotFocus="UIElement_OnGotFocus" LostFocus="UIElement_OnLostFocus"
IsEditable="True"
Grid.Column="1"
ItemsSource="{Binding StreetOthers, Mode=TwoWay}"
Text="{Binding SelectedStreetOthers, Mode=TwoWay, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"
Height="36" Width="250"
HorizontalAlignment="Left"
VerticalAlignment="Top"
FontSize="18"
Margin="10 0 0 0"
Padding="5" />
</ScrollViewer>
And the code behind :
public double RememberedPoisitionOfScrollBar { get; set; }
private void UIElement_OnGotFocus(object sender, RoutedEventArgs e)
{
Application.Current.Dispatcher.InvokeAsync(() =>
{
if (!Keyboard.IsOpen)
{
Keyboard.IsOpen = true;
KeyBoardScrollViewer.VerticalScrollBarVisibility = ScrollBarVisibility.Visible;
}
Point relativePoint = ((FrameworkElement)sender).TransformToAncestor(KeyBoardScrollViewer)
.Transform(new Point(0, 0));
KeyBoardScrollViewer.CanContentScroll = false;
RememberedPoisitionOfScrollBar = relativePoint.Y - 5;
var offset = KeyBoardScrollViewer.VerticalOffset + relativePoint.Y - 5; //sender.OccludedRect.Top
KeyBoardScrollViewer.ScrollToVerticalOffset(offset);
//KeyBoardScrollViewer.ScrollToVerticalOffset(offset);
KeyBoardScrollViewer.UpdateLayout();
});
Keyboard.Width = this.ActualWidth;
}
private void UIElement_OnLostFocus(object sender, RoutedEventArgs e)
{
Keyboard.IsOpen = false;
KeyBoardScrollViewer.ScrollToVerticalOffset(KeyBoardScrollViewer.VerticalOffset - RememberedPoisitionOfScrollBar);
KeyBoardScrollViewer.UpdateLayout();
KeyBoardScrollViewer.VerticalScrollBarVisibility = ScrollBarVisibility.Hidden;
}
This solution works, the combobox is moved to the top of the view, but I have a strange behavior. The combobox "content", remains in the original position, and is not updated according to the new position of the combobox .
Here is the picture :
According to Telerik admins (old post) it is a WPF problem, but it has to be a workaround, no? Any idea is useful!
Thank you!
In the scroll viewer, subscribe to the ScrollChanged event:
ScrollChanged="KeyboardScrollView_OnScrollChanged"
and in the codebehind, if the dropdown is open when scroll occurs, close it and reopen it:
private void KeyboardScrollView_OnScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (TheComboBox.IsDropDownOpen)
{
TheComboBox.IsDropDownOpen = false;
TheComboBox.IsDropDownOpen = true;
}
}
Related
I'm working on a dynamic C# WPF application (on Windows 10) that uses a fullscreen Grid. Controls are added to the grid dynamically at runtime (which are managed in a Dictionary<>) and I recently added code to move the controls along the grid with the mouse (also at runtime) using a TranslateTransform (which I am now doubting the viability of).
Is there a way I can prevent the controls from overlapping or "sharing space" on the grid when moving them? In other words, adding some sort of collision detection. Would I use an if statement to check the control margin ranges or something? My move events are shown below:
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
// Orientation variables:
public bool _isInDrag = false;
public Dictionary<object, TranslateTransform> PointDict = new Dictionary<object, TranslateTransform();
public Point _anchorPoint;
public Point _currentPoint;
public MainWindow()
{
InitializeComponent();
}
public static void Control_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (_isInDrag)
{
var element = sender as FrameworkElement;
element.ReleaseMouseCapture();
_isInDrag = false;
e.Handled = true;
}
}
public static void Control_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var element = sender as FrameworkElement;
_anchorPoint = e.GetPosition(null);
element.CaptureMouse();
_isInDrag = true;
e.Handled = true;
}
public static void Control_MouseMove(object sender, MouseEventArgs e)
{
if (_isInDrag)
{
_currentPoint = e.GetPosition(null);
TranslateTransform tt = new TranslateTransform();
bool isMoved = false;
if (PointDict.ContainsKey(sender))
{
tt = PointDict[sender];
isMoved = true;
}
tt.X += _currentPoint.X - _anchorPoint.X;
tt.Y += (_currentPoint.Y - _anchorPoint.Y);
(sender as UIElement).RenderTransform = tt;
_anchorPoint = _currentPoint;
if (isMoved)
{
PointDict.Remove(sender);
}
PointDict.Add(sender, tt);
}
}
}
MainWindow.xaml (example):
<Window x:Name="MW" x:Class="MyProgram.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MyProgram"
mc:Ignorable="d"
Title="MyProgram" d:DesignHeight="1080" d:DesignWidth="1920" ResizeMode="NoResize" WindowState="Maximized" WindowStyle="None">
<Grid x:Name="MyGrid" />
<Image x:Name="Image1" Source="pic.png" Margin="880,862,0,0" Height="164" Width="162" HorizontalAlignment="Left" VerticalAlignment="Top" MouseLeftButtonDown="Control_MouseLeftButtonDown" MouseLeftButtonUp="Control_MouseLeftButtonUp" MouseMove="Control_MouseMove" />
<TextBox x:Name="Textbox1" Margin="440,560,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" MouseLeftButtonDown="Control_MouseLeftButtonDown" MouseLeftButtonUp="Control_MouseLeftButtonUp" MouseMove="Control_MouseMove" />
</Window>
Edit: It seems that moving a control with a TranslateTransform does not change the margin for that control. Not sure why.
Edit 2: Not getting much traction. If anyone needs clarification on anything, please ask.
Edit 3: Pretty sure I can't use TranslateTransform because it does not change the margin of a given control. Is there an alternative?
Edit 4: Added some 'boilerplate' code for those who want to copy & paste. Let me know if you have any questions about it.
TL;DR: Demo from the bottom of this answer
When you want to modify your UI without adding event handlers to every single control, the way to go is with Adorners. Adorners are (as the name implies) controls that adorn another control to add additional visuals or as in your case functionality. Adorners reside in an AdornerLayer which you can either add yourself or use the one that every WPF Window already has. The AdornerLayer is on top of all your other controls.
You never mentioned what should happen when the user lets go of the mouse button when controls overlap so I just reset the control to its original position if that happens.
At this point I'd usually explain what to keep in mind when moving controls but since your original example even contains the CaptureMouse people usually forget, I think you'll understand the code without further explanation :)
A couple of things you might want to add / improve:
A snap to grid feature (pixel precise movement can be a bit overwhelming for the average user)
Take RenderTransform, LayoutTransform and non-rectangular shapes (if needed) into account when calculating the overlap
Move the editing functionality (enable, disable, etc.) into a separate control and add a dedicated AdornerLayer
Disable interactive controls (Buttons, TextBoxes, ComboBoxes, etc.) in edit-mode
Cancel movement when the user presses Esc
Restrict movement to the bounds of the parent container done
Move the active Adorner to the top of the AdornerLayer
Let the user move multiple controls at once (typically by selecting them with Ctrl)
Previously unanswered question:
Are you saying controls are no longer assigned a margin when using TranslateTransform?
Not at all - You could use a combination of Grid.Row, Grid.Column, Margin, RenderTransform and LayoutTransform but then it would be a nightmare to determine where the control is actually displayed. If you stick with one (In this case for example Margin or LayoutTransform) it is much easier to work with and keep track of. If you ever find yourself in a situation where you need more than one at the same time, you would have to find the actual position by determining the corners of the control by transforming (0, 0) and (ActualWidth, ActualHeight) with TransformToAncestor. Trust me, you don't want to go there - keep it simple, stick with one of them.
The below code is not the "holy grail of how to move things" but it should give you an idea of how to do it and what else you could do with it (resize, rotate, remove controls, etc.). The layouting is based purely on the Left and Top margin of the controls. It shouldn't be to hard to swap out all Margins for LayoutTransforms if you prefer that, as long as you keep it consistent.
Move Adorner
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
public class MoveAdorner : Adorner
{
// The parent of the adorned Control, in your case a Grid
private readonly Panel _parent;
// Same as "AdornedControl" but as a FrameworkElement
private readonly FrameworkElement _child;
// The visual overlay rectangle we can click and drag
private readonly Rectangle _rect;
// Our own collection of child elements, in this example only _rect
private readonly UIElementCollection _visualChildren;
private bool _down;
private Point _downPos;
private Thickness _downMargin;
private List<Rect> _otherRects;
protected override int VisualChildrenCount => _visualChildren.Count;
protected override Visual GetVisualChild(int index)
{
return _visualChildren[index];
}
public MoveAdorner(FrameworkElement adornedElement) : base(adornedElement)
{
_child = adornedElement;
_parent = adornedElement.Parent as Panel;
_visualChildren = new UIElementCollection(this,this);
_rect = new Rectangle
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
StrokeThickness = 1,
};
SetColor(Colors.LightGray);
_rect.MouseLeftButtonDown += RectOnMouseLeftButtonDown;
_rect.MouseLeftButtonUp += RectOnMouseLeftButtonUp;
_rect.MouseMove += RectOnMouseMove;
_visualChildren.Add(_rect);
}
private void SetColor(Color color)
{
_rect.Fill = new SolidColorBrush(color) {Opacity = 0.3};
_rect.Stroke = new SolidColorBrush(color) {Opacity = 0.5};
}
private void RectOnMouseMove(object sender, MouseEventArgs args)
{
if (!_down) return;
Point pos = args.GetPosition(_parent);
UpdateMargin(pos);
}
private void UpdateMargin(Point pos)
{
double deltaX = pos.X - _downPos.X;
double deltaY = pos.Y - _downPos.Y;
Thickness newThickness = new Thickness(_downMargin.Left + deltaX, _downMargin.Top + deltaY, 0, 0);
//Restrict to parent's bounds
double leftMax = _parent.ActualWidth - _child.ActualWidth;
double topMax = _parent.ActualHeight - _child.ActualHeight;
newThickness.Left = Math.Max(0, Math.Min(newThickness.Left, leftMax));
newThickness.Top = Math.Max(0, Math.Min(newThickness.Top, topMax));
_child.Margin = newThickness;
bool overlaps = CheckForOverlap();
SetColor(overlaps ? Colors.Red : Colors.Green);
}
// Check the current position for overlaps with all other controls
private bool CheckForOverlap()
{
if (_otherRects == null || _otherRects.Count == 0)
return false;
Rect thisRect = GetRect(_child);
foreach(Rect otherRect in _otherRects)
if (thisRect.IntersectsWith(otherRect))
return true;
return false;
}
private Rect GetRect(FrameworkElement element)
{
return new Rect(new Point(element.Margin.Left, element.Margin.Top), new Size(element.ActualWidth, element.ActualHeight));
}
private void RectOnMouseLeftButtonUp(object sender, MouseButtonEventArgs args)
{
if (!_down) return;
Point pos = args.GetPosition(_parent);
UpdateMargin(pos);
if (CheckForOverlap())
ResetMargin();
_down = false;
_rect.ReleaseMouseCapture();
SetColor(Colors.LightGray);
}
private void ResetMargin()
{
_child.Margin = _downMargin;
}
private void RectOnMouseLeftButtonDown(object sender, MouseButtonEventArgs args)
{
_down = true;
_rect.CaptureMouse();
_downPos = args.GetPosition(_parent);
_downMargin = _child.Margin;
// The current position of all other elements doesn't have to be updated
// while we move this one so we only determine it once
_otherRects = new List<Rect>();
foreach (FrameworkElement child in _parent.Children)
{
if (ReferenceEquals(child, _child))
continue;
_otherRects.Add(GetRect(child));
}
}
// Whenever the adorned control is resized or moved
// Update the size of the overlay rectangle
// (Not 100% necessary as long as you only move it)
protected override Size MeasureOverride(Size constraint)
{
_rect.Measure(constraint);
return base.MeasureOverride(constraint);
}
protected override Size ArrangeOverride(Size finalSize)
{
_rect.Arrange(new Rect(new Point(0,0), finalSize));
return base.ArrangeOverride(finalSize);
}
}
Usage
private void DisableEditing(Grid theGrid)
{
// Remove all Adorners of all Controls
foreach (FrameworkElement child in theGrid.Children)
{
var layer = AdornerLayer.GetAdornerLayer(child);
var adorners = layer.GetAdorners(child);
if (adorners == null)
continue;
foreach(var adorner in adorners)
layer.Remove(adorner);
}
}
private void EnableEditing(Grid theGrid)
{
foreach (FrameworkElement child in theGrid.Children)
{
// Add a MoveAdorner for every single child
Adorner adorner = new MoveAdorner(child);
// Add the Adorner to the closest (hierarchically speaking) AdornerLayer
AdornerLayer.GetAdornerLayer(child).Add(adorner);
}
}
Demo XAML
<Grid>
<Button Content="Enable Editing" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="100" Click="BtnEnable_Click"/>
<Button Content="Disable Editing" HorizontalAlignment="Left" Margin="115,10,0,0" VerticalAlignment="Top" Width="100" Click="BtnDisable_Click"/>
<Grid Name="grid" Background="AliceBlue" Margin="10,37,10,10">
<Button Content="Button" HorizontalAlignment="Left" Margin="83,44,0,0" VerticalAlignment="Top" Width="75"/>
<Ellipse Fill="#FFF4F4F5" HorizontalAlignment="Left" Height="100" Margin="207,100,0,0" Stroke="Black" VerticalAlignment="Top" Width="100"/>
<Rectangle Fill="#FFF4F4F5" HorizontalAlignment="Left" Height="100" Margin="33,134,0,0" Stroke="Black" VerticalAlignment="Top" Width="100"/>
</Grid>
</Grid>
Expected Result
When editing is disabled controls cannot be moved, interactive controls can be clicked / interacted with without obstruction. When editing mode is enabled, each control is overlayed with an adorner that can be moved. If the target position overlaps with another control, the adorner will turn red and the margin will be reset to the initial position if the user lets go of the mouse button.
There is no other way then to check if there control exists on place where you are moving.
Since you are moving UI elements a lot it is better to use canvas instead of grid where you can layout elements with Top and Left parameters.
Here is modified code of yours that do that
public partial class MainWindow : Window
{
public bool _isInDrag = false;
public Dictionary<object, TranslateTransform> PointDict = new Dictionary<object, TranslateTransform>();
public Point _anchorPoint;
public Point _currentPoint;
public MainWindow()
{
InitializeComponent();
}
public void Control_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (_isInDrag)
{
var element = sender as FrameworkElement;
element.ReleaseMouseCapture();
Panel.SetZIndex(element, 0);
_isInDrag = false;
e.Handled = true;
}
}
public void Control_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var element = sender as FrameworkElement;
_anchorPoint = e.GetPosition(null);
element.CaptureMouse();
Panel.SetZIndex(element, 10);
_isInDrag = true;
e.Handled = true;
}
public void Control_MouseMove(object sender, MouseEventArgs e)
{
if (_isInDrag)
{
_currentPoint = e.GetPosition(null);
FrameworkElement fw = sender as FrameworkElement;
if (fw != null)
{
FrameworkElement fwParent = fw.Parent as FrameworkElement;
if (fwParent != null)
{
Point p = new Point(_currentPoint.X - _anchorPoint.X + Canvas.GetLeft((sender as UIElement)), _currentPoint.Y - _anchorPoint.Y + Canvas.GetTop((sender as UIElement)));
List<HitTestResult> lst = new List<HitTestResult>()
{
VisualTreeHelper.HitTest(fwParent , p),
VisualTreeHelper.HitTest(fwParent, new Point(p.X + fw.Width, p.Y)),
VisualTreeHelper.HitTest(fwParent, new Point(p.X, p.Y + fw.Height)),
VisualTreeHelper.HitTest(fwParent, new Point(p.X + fw.Width, p.Y +fw.Height)),
};
bool success = true;
foreach (var item in lst)
{
if (item != null)
{
if (item.VisualHit != sender && item.VisualHit != fwParent && fw.IsAncestorOf(item.VisualHit) == false)
{
success = false;
break;
}
}
}
if (success)
{
Canvas.SetTop((sender as UIElement), p.Y);
Canvas.SetLeft((sender as UIElement), p.X);
_anchorPoint = _currentPoint;
}
}
}
}
}
}
Xaml
<Window x:Class="ControlsOverlapWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ControlsOverlapWpf"
mc:Ignorable="d"
Title="MyProgram" d:DesignHeight="500" d:DesignWidth="500" ResizeMode="NoResize" WindowState="Normal" WindowStyle="None">
<Canvas Background="Pink">
<Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown" PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
<Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown" PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
<Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown" PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
<Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown" PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
<Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown" PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
<Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown" PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
</Canvas>
</Window>
Here is my scenario. My table has fixed number of columns, say 2, and initially, it has only one visible row, but when the focus is on the last column of row 1 and the user press 'tab', row 2 will be made visible.
My problem is that I can't dynamically select the row I want to make visible because I have to specify its x:Name during compilation.
Below is my current work.
.xaml file
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal" x:Name="SP1">
<TextBox Text="1-1"/>
<TextBox Text="1-2" KeyDown="showNextLine"/>
</StackPanel>
<StackPanel Orientation="Horizontal" x:Name="SP2" Visibility="Collapsed">
<TextBox Text="2-1"/>
<TextBox Text="2-2"/>
</StackPanel>
<!--the remaining rows...-->
</StackPanel>
.cs file
private int lastRowIndex = 1;
private void showNextLine(object sender, KeyRoutedEventArgs e)
{
lastRowIndex++;
string nextLineName = "SP" + lastRowIndex.ToString();
nextLineName.Visibility = Visibility.Visible; // which causes error because nextLineName is string instead of StackPanel
}
Besides, my current implementation is to create 50 rows and make the last 49 invisible initially, and I am open to any method to group all the TextBox more systematically or flexibly.
Thanks for reading.
You could give the parent StackPanel an x:Name or keep a reference to it if you create it dynamically:
<StackPanel x:Name="root" Orientation="Vertical">
<StackPanel Orientation="Horizontal" x:Name="SP1">
<TextBox Text="1-1"/>
<TextBox Text="1-2" KeyDown="showNextLine"/>
</StackPanel>
<StackPanel Orientation="Horizontal" x:Name="SP2" Visibility="Collapsed">
<TextBox Text="2-1"/>
<TextBox Text="2-2"/>
</StackPanel>
<!--the remaining rows...-->
</StackPanel>
...and then get a reference to a child StackPanel using the Children property and some basic LINQ:
private void showNextLine(object sender, KeyRoutedEventArgs e)
{
lastRowIndex++;
string nextLineName = "SP" + lastRowIndex.ToString();
StackPanel child = root.Children.OfType<StackPanel>().FirstOrDefault(x => x.Name == nextLineName);
if (child != null)
child.Visibility = Visibility.Visible;
}
How could I create a StackPanel dynamically?
Like this:
var sp = new StackPanel { Name = "SP3", Orientation = Orientation.Horizontal, Visibility = Visibility.Collapsed };
sp.Children.Add(new TextBlock { Text = "3-1" });
var txt = new TextBlock() { Text = "3-2" };
txt.KeyDown += showNextLine;
sp.Children.Add(txt);
root.Children.Add(sp);
I can't think of an easy way to do the first part of this (since you have 50 stack panels), but if you put all of them in a dictionary, then you could update them using just the key.
Here's the dictionary part done manually:
Dictionary<int, StackPanel> myStackPanels = new Dictionary<int, StackPanel>();
myStackPanels.Add(1, SP1);
myStackPanels.Add(2, SP2);
Then, here's what ShowNextLine would look like:
private void showNextLine(object sender, KeyRoutedEventArgs e)
{
lastRowIndex++;
// Modify the StackPanel whose key is lastRowIndex;
myStackPanels[lastRowIndex] = Visibility.Visible;
}
How is it possible to reach an input field within a ScrollViewer when the input field is overlapped by the soft keyboard?
This scenario is easily reproduced:
Create a new page with a ScrollViewer containing some TextBoxes. Make as many TextBoxes as you need until you need to scroll the page to reach the last three TextBoxes.
<ScrollViewer>
<StackPanel Orientation="Vertical">
<TextBox Margin="20" />
<TextBox Margin="20" />
<TextBox Margin="20" />
..
<TextBox Margin="20" />
<TextBox Margin="20" />
<TextBox Margin="20" PlaceholderText="3" />
<TextBox Margin="20" PlaceholderText="2" />
<TextBox Margin="20" PlaceholderText="1" />
</StackPanel>
</ScrollViewer>
Start the app and tap into "Placeholder 3". The keyboard pops up and overlaps "Paceholder 2" and "Placeholder 1".
How can I improve the layout so I can reach these TextBoxes ("1" and "2") without closing and re-opening the keyboard all the time?
An example that shows a working solution can be found on every WindowsPhone: Settings => VPN => Enable VPN => Add new profile => Click in any of the TextBoxes and you'll see that you can scroll to every part of the layout although the soft keyboard is up.
Been awhile on this question but for others who may be looking for a good solution here is what I did.
Subscribe to the keyboard show and hide events and size the height of the scrollviewer based on when the keyboard is showing or hiding.
Xaml
<ScrollViewer x:Name="scrlvwrKBScroll" VerticalScrollMode="Enabled">
<StackPanel Orientation="Vertical">
<TextBox Margin="20" />
<TextBox Margin="20" />
<TextBox Margin="20" />
..
<TextBox Margin="20" />
<TextBox Margin="20" />
<TextBox Margin="20" PlaceholderText="3" />
<TextBox Margin="20" PlaceholderText="2" />
<TextBox Margin="20" PlaceholderText="1" />
</StackPanel>
</ScrollViewer>
C#
public Constructor()
{
this.InitializeComponent()
InputPane.GetForCurrentView().Showing += Keyboard_OnShow;
InputPane.GetForCurrentView().Hiding += Keyboard_OnHide;
}
private void Keyboard_OnShow(InputPane sender, InputPaneVisibilityEventArgs args)
{
this.scrllvwrKBScroll.Height = this.ActualHeight - args.OccludedRect.Height - 50;
}
private void Keyboard_OnHide(InputPane sender, InputPaneVisibilityEventArgs args)
{
this.scrllvwrKBScroll.height = this.ActualHeight;
}
There may be a better way to adjust the height based on the heights of the containers you are using but this is what I used to get my application to work.
I also encountered this problem whenever a Page with a BottomAppBar is displaced in the layout from the root visual. This can be caused by a Margin or Padding on a wrapper element.
Broken visual tree:
Window.Current.Content Frame
Border with 1px Margin
ContentPresenter
Page with BottomAppBar
I could find no "non-disgusting" workaround, but adjusting the offset directly on the root ScrollViewer did work for me. See UWPMobileScrollIssue for a full repro and workaround.
// ...snip...
namespace UWPFocusTestApp
{
sealed partial class App : Application
{
// ...snip...
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
// ...snip...
if (rootFrame == null)
{
// ...snip...
// Place the frame in the current Window
Window.Current.Content = rootFrame;
#region WORKAROUND
if (AnalyticsInfo.VersionInfo.DeviceFamily == "Windows.Mobile")
{
InputPane.GetForCurrentView().Showing += InputPane_Showing;
}
#endregion
}
// ...snip...
}
#region WORKAROUND
private void InputPane_Showing(InputPane sender, InputPaneVisibilityEventArgs args)
{
// we only need to hook once
InputPane.GetForCurrentView().Showing -= InputPane_Showing;
var frame = (Frame)Window.Current.Content;
// Find root ScrollViewer
DependencyObject cNode = frame;
while (true)
{
var parent = VisualTreeHelper.GetParent(cNode);
if (parent == null)
{
break;
}
cNode = parent;
}
var rootScrollViewer = (ScrollViewer)cNode;
// Hook ViewChanged to update scroll offset
bool hasBeenAdjusted = false;
rootScrollViewer.ViewChanged += (_1, svargs) =>
{
// once the scroll is removed, clear flag
if (rootScrollViewer.VerticalOffset == 0)
{
hasBeenAdjusted = false;
return;
}
// if we've already adjusted, bail.
else if (hasBeenAdjusted)
{
return;
}
var appBar = ((Page)frame.Content)?.BottomAppBar;
if (appBar == null)
{
return;
}
hasBeenAdjusted = true;
rootScrollViewer.ChangeView(null, rootScrollViewer.VerticalOffset + appBar.ActualHeight, null);
};
}
#endregion
// ...snip...
}
}
I'm using a list of textboxes for a registering document in a WP8 app.
The number of textboxes is quite large, so the user has to scroll between them.
To navigate between one field to another, I added two applicationbarIcons, next and previous. Pressing on next will change the focus to the next textbox from list, and scroll the content of the scroll viewer with the height of the textbox (in this case 50).
However, sometimes, when switching the focus to the element bellow, the keyboard covers the text box. (the content doesn't scroll up).
Is there a way to force the textbox to move above the keyboard, even if it is in a scroll view?
<ScrollViewer x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<StackPanel>
<TextBlock Text="{Binding Source={StaticResource LocalizedStrings}, Path=LocalizedResources.STRING_CONTACT}" Margin="10,5" FontWeight="SemiBold" Foreground="#878780"></TextBlock>
<StackPanel Margin="10,5" Height="190" Background="#F4F3F4">
<TextBox LostFocus="firstNameTxt_LostFocus_1" GotFocus="firstNameTxt_GotFocus_1" Margin="0,-7" FontSize="23" x:Name="firstNameTxt" BorderThickness="0" Background="Transparent" InputScope="PersonalFullName"><TextBox>
<TextBox LostFocus="firstNameTxt_LostFocus_1" GotFocus="firstNameTxt_GotFocus_1" Margin="0,-7" FontSize="23" x:Name="lastNameTxt" BorderThickness="0" Background="Transparent" InputScope="PersonalFullName"></my:DefaultTextBox>
<TextBox LostFocus="firstNameTxt_LostFocus_1" GotFocus="firstNameTxt_GotFocus_1" Margin="0,-7" FontSize="23" x:Name="MobileTxt" BorderThickness="0" InputScope="Number" Background="Transparent" ></TextBox>
<TextBox LostFocus="firstNameTxt_LostFocus_1" GotFocus="firstNameTxt_GotFocus_1" Margin="0,-7" FontSize="23" x:Name="EmailTxt" BorderThickness="0" Background="Transparent">
</StackPanel>
</ScrollViewer>
Code behind:
void left_Click(object sender, EventArgs e)
{
int index = this.controls.IndexOf(currentControl) - 1;
if (index == -1)
{
this.Focus();
return;
}
currentControl = this.controls[index];
ContentPanel.ScrollToVerticalOffset(ContentPanel.VerticalOffset - 50);
currentControl.Focus();
}
This is a common issue on WP8. When a textbox is focused, it will translate Application 's RootVisual to bring it into view. This doesn't work well in some cases (when clipboard is on, or in your case). A workaround is manually translating RootVisual to a desired vertical offset on GotFocus and LostFocus events of TextBox.
private void TranslateRootVisualY(int yNew)
{
var rootFrame = Application.Current.RootVisual as PhoneApplicationFrame;
rootFrame.RenderTransform = new CompositeTransform() {TranslateY = yNew};
}
In your case, you can eliminate the automatic translation and make ScrollViewer scroll to desired offset in GotFocus event:
private void firstNameTxt_GotFocus_1(object sender, RoutedEventArgs e)
{
TranslateRootVisualY(0);
Dispatcher.BeginInvoke(() =>{
double destOffset;
//...calculate destination offset
ContentPanel.ScrollToVerticalOffset(destOffset);
});
}
destOffset can be calculated from sender and other function like GetRectFromCharacterIndex
I have a list box displaying the names of help topics which can be added to and the names of the topics changed. Originally it was just displaying strings, but to get the inline editing working I changed it to use a custom type consisting of a string and an InEdit property so the UI can determine whether to display the TextBlock or TextBox:
XAML:
<ListBox ItemsSource="{Binding HelpTopics, Mode=TwoWay}"
SelectedValuePath="Description"
SelectedValue="{Binding SelectedPageId, Mode=TwoWay}"
SelectionChanged="ListBox_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding Description, Mode=TwoWay}"
VerticalAlignment="Center"
MouseLeftButtonUp="TopicTextBlock_MouseLeftButtonUp"
Visibility="{Binding InEdit, Converter={StaticResource boolToVisibilityConverter}, ConverterParameter=contra}"/>
<TextBox Text="{Binding Description, Mode=TwoWay}"
Visibility="{Binding InEdit, Converter={StaticResource boolToVisibilityConverter}, ConverterParameter=pro}"
LostFocus="EditTopicTextBox_LostFocus"
HorizontalAlignment="Stretch" VerticalAlignment="Center"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Margin="5" Content="Add Topic" Command="{Binding AddTopicCommand}"/>
HelpTopics is an ObservableCollection<EditableHelpTopic>.
SelectedPageId is a string.
boolToVisibilityConverter is a converter that does what it says.
What works:
Adding a topic creates a new item and adds it to the list and put the item in to edit mode.
Double clicking on an existing item puts that item into edit mode sets the focus to the TextBox and selects all the text so it can be overwritten.
When the TextBox loses focus the edit is saved and the display returns to the TextBlock.
What doesn't work:
When a new topic is added the TextBox should have focus and the text selected so the user can enter a new name.
So my question is is there a point in the code or an event where I know that the TextBox has been created and is visible so I can set focus and select its contents. I've tried hooking into the SelectionChanged event but when that fires the TextBox hasn't yet been displayed. I also added an event to the OnAddTopicExecute method in the view model which I handled in the view, but again that fired before the TextBox was visible.
Below is the code that supports the above XAML. I've tried to cut it down, but there still seems to be a lot of it, so you can skip this if you're not interested ;)
Code behind:
private DateTime lastClickTime = DateTime.MinValue;
private Point lastClickPosition;
private void TopicTextBlock_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
UIElement element = sender as UIElement;
if ((DateTime.Now - this.lastClickTime).TotalMilliseconds > 300)
{
this.lastClickPosition = e.GetPosition(element);
this.lastClickTime = DateTime.Now;
}
else
{
Point position = e.GetPosition(element);
if (Math.Abs(this.lastClickPosition.X - position.X) < 4 && Math.Abs(this.lastClickPosition.Y - position.Y) < 4)
{
var textBlock = sender as TextBlock;
var editableHelpTopic = textBlock.DataContext as EditableHelpTopic;
editableHelpTopic.InEdit = true;
var parent = textBlock.Parent as Grid;
TextBox textBox = parent.Children.First(c => c.GetType() == typeof(TextBox)) as TextBox;
textBox.Focus();
textBox.SelectAll();
}
}
}
private void EditTopicTextBox_LostFocus(object sender, RoutedEventArgs e)
{
var textBox = sender as TextBox;
var editableHelpTopic = textBox.DataContext as EditableHelpTopic;
editableHelpTopic.InEdit = false;
if (!textBox.Text.Equals(editableHelpTopic.Description))
{
this.editViewModel.RenameTopic(textBox.Text);
}
}
View Model:
public EditViewModel()
{
...
this.AddTopicCommand = new DelegateCommand(this.OnAddTopicExecute, this.OnAddTopicCanExecute);
...
}
where DelegateCommand is an implemetation of ICommand.
private void OnAddTopicExecute(object parameter)
{
var newTopic = new EditableHelpTopic
{
Description = "NewTopic",
InEdit = true
};
this.HelpTopics.Add(newTopic);
this.SelectedPageId = newTopic.Description;
}
Definitions:
public class EditableHelpTopic : INotifyPropertyChanged
{
public bool InEdit { ... }
public string Description { ... }
}
It turned out to be simpler than I thought.
I just needed to add a Loaded event handler to the TextBox:
private void EditTopicTextBox_Loaded(object sender, RoutedEventArgs e)
{
var textBox = sender as TextBox;
var editableHelpTopic = textBox.DataContext as EditableHelpTopic;
if (editableHelpTopic.InEdit)
{
textBox.Focus();
textBox.SelectAll();
}
}