I'm trying to achieve the equivalent of a WinForms ListView with its View property set to View.List. Visually, the following works fine. The file names in my Listbox go from top to bottom, and then wrap to a new column.
Here's the basic XAML I'm working with:
<ListBox Name="thelist"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding}"
ScrollViewer.VerticalScrollBarVisibility="Disabled">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True"
Orientation="Vertical" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
However, default arrow key navigation does not wrap. If the last item in a column is selected, pressing the down arrow does not go to the first item of the next column.
I tried handling the KeyDown event like this:
private void thelist_KeyDown( object sender, KeyEventArgs e ) {
if ( object.ReferenceEquals( sender, thelist ) ) {
if ( e.Key == Key.Down ) {
e.Handled = true;
thelist.Items.MoveCurrentToNext();
}
if ( e.Key == Key.Up ) {
e.Handled = true;
thelist.Items.MoveCurrentToPrevious();
}
}
}
This produces the last-in-column to first-in-next-column behavior that I wanted, but also produces an oddity in the left and right arrow handling. Any time it wraps from one column to the next/previous using the up/down arrows, a single subsequent use of the left or right arrow key moves the selection to the left or right of the item that was selected just before the wrap occured.
Assume the list is filled with strings "0001" through "0100" with 10 strings per column. If I use the down arrow key to go from "0010" to "0011", then press the right arrow key, selection moves to "0020", just to the right of "0010". If "0011" is selected and I use the up arrow key to move selection to "0010", then a press of the right arrow keys moves selection to "0021" (to the right of "0011", and a press of the left arrow key moves selection to "0001".
Any help achieving the desired column-wrap layout and arrow key navigation would be appreciated.
(Edits moved to my own answer, since it technically is an answer.)
It turns out that when it wraps around in my handling of the KeyDown event, selection changes to the correct item, but focus is on the old item.
Here is the updated KeyDown eventhandler. Because of Binding, the Items collection returns my actual items rather than ListBoxItems, so I have to do a call near the end to get the actual ListBoxItem I need to call Focus() on. Wrapping from last item to first and vice-versa can be achieved by swapping the calls of MoveCurrentToLast() and MoveCurrentToFirst().
private void thelist_KeyDown( object sender, KeyEventArgs e ) {
if ( object.ReferenceEquals( sender, thelist ) ) {
if ( thelist.Items.Count > 0 ) {
switch ( e.Key ) {
case Key.Down:
if ( !thelist.Items.MoveCurrentToNext() ) {
thelist.Items.MoveCurrentToLast();
}
break;
case Key.Up:
if ( !thelist.Items.MoveCurrentToPrevious() ) {
thelist.Items.MoveCurrentToFirst();
}
break;
default:
return;
}
e.Handled = true;
ListBoxItem lbi = (ListBoxItem) thelist.ItemContainerGenerator.ContainerFromItem( thelist.SelectedItem );
lbi.Focus();
}
}
}
You should be able to do it without the event listener using KeyboardNavigation.DirectionalNavigation, e.g.
<ListBox Name="thelist"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding}"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
KeyboardNavigation.DirectionalNavigation="Cycle">
Related
My usercontrol has Listbox with Images as ListboxItems, here i'm facing an issue when i navigate listbox items (Images) using "Arrow Keys",i could'n navigate the items that is present in Next row, say for example ,List box contains rows of images*("I have used WrapPanel")*, if i navigate an images using Right arrow key i cant able to move to next row,
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="KeyboardNavigation.DirectionalNavigation" Value="Cycle" />
<Setter Property="IsTabStop" Value="True" />
</Style>
</ListBox.ItemContainerStyle>
Based on this answer which almost worked, but not quite.
Put a KeyDown event on your ListBox and use its ItemsCollection to select the next or previous element when you see a Right or Left keypress.
That moves the selection, but not the keyboard focus (dotted line), so you must also call MoveFocus on the element that has Keyboard focus.
private void ListBox_KeyDown( object sender, KeyEventArgs e )
{
var list = sender as ListBox;
switch( e.Key )
{
case Key.Right:
if( !list.Items.MoveCurrentToNext() ) list.Items.MoveCurrentToLast();
break;
case Key.Left:
if( !list.Items.MoveCurrentToPrevious() ) list.Items.MoveCurrentToFirst();
break;
}
e.Handled = true;
if( list.SelectedItem != null )
{
(Keyboard.FocusedElement as UIElement).MoveFocus( new TraversalRequest( FocusNavigationDirection.Next ) );
}
}
Finally, make sure you have IsSynchronizedWithCurrentItem="True" on your ListBox.
This will give you the wrap-around behaviour that you want.
In my WPF application I have one datagrid and one textbox. In the textChanged event of the textbox, I put this:
myDatagrid.ItemsSource =
myListOfObjects.Where(item => item.Name.Contains(MyTextBox.Text)); //Filter
if (myDatagrid.Items.Count > 0) // If no itens, then do nothing
{
myDatagrid.SelectedIndex = 0; // If has at least one item, select the first
}
myDatagrid.Items.Refresh();
Note that I force the selection when the text changes, in the first row of the DataGrid.
But unfortunately, the color of the row does not change to blue, making it hard to see the selection.
I realy need this, because in the PreviewKeyDown event of the textbox I have this:
private void myTextBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Up)
{
if (!(myDataGrid.SelectedIndex <= 0))
{
myDataGrid.SelectedIndex--; // Go one position Up
}
}
if (e.Key == Key.Down)
{
if (!(myDataGrid.SelectedIndex == myDataGrid.Items.Count - 1))
{
myDataGrid.SelectedIndex++; // Go one position Down
}
}
}
So, when the textbox is focused and the user press the Up or the Down key, the selection does not appear to change.
Any idea of how I can make the selected item on the datagrid change it's color to blue?
Other thing: in my virtual machine, it works!! With the same code! How it's possible?
I think that is the aeroglass, but I change the theme to the Windows 7 Basic (same in the virtual machine) and still don't work.
Thanks, and sorry for my english.
Could you try using SelectedItem? you could always create a new property and bind to this and then set this item directly rather than using the selected index. Hopefully this would trigger any additional logic in the DataGrid control :)
//Declare property outside of method
public ObjectType SelectedItem { get; set; }
//Set datacontext on load
DataContext = this;
myDatagrid.ItemsSource = myListOfObjects.Where(item => item.Name.Contains(MyTextBox.Text)); //Filter
if (myDatagrid.Items.Count > 0) // If no itens, then do nothing
{
SelectedItem = myDatagrid.ItemSource[0]; // If has at least one item, select the first
}
myDatagrid.Items.Refresh();
Also don't forget to set your binding!
SelectedItem="{Binding SelectedItem}"
hope that helps!
Is there some way to set the Height attribute of a WPF multi-select ListBox to be a multiple of the item height, similar to setting the size attribute of an html select element?
I have a business requirement to not have half an item showing at the bottom of the list (if it's a long list with a scrollbar), and not have extra white space at the bottom (if it's a short list with all items showing), but the only method I can find to do this is to just keep tweaking the Height until it looks about right.
(What else have I tried? I've asked colleagues, searched MSDN and StackOverflow, done some general Googling, and looked at what VS Intellisense offered as I edited the code. There's plenty of advice out there about how to set the height to fit the ListBox's container, but that's the opposite of what I'm trying to do.)
Yeah, one could imagine there would be an easier way to do it (a single snapToWholeElement property). I couldn't find this property as well.
To achieve your requirement, I've wrote a little logic. Basically, In my Windows object I've a public property lbHeight which is calculate the listbox height by calculating the height of each individual item.
First, let's take a look at the XAML:
<Window
x:Class="SO.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="120" SizeToContent="Height"
Title="SO Sample"
>
<StackPanel>
<ListBox x:Name="x_list" Height="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}, Path=lbHeight}" >
<ListBox.ItemTemplate>
<DataTemplate>
<Border x:Name="x" Background="Gray" Margin="4" Padding="3">
<TextBlock Text="{Binding}" />
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Window>
Note that the ItemTemplate is somewhat non trivial. One important thing to notice is that I gave this item a Name - so I can find it later.
In the code-behind constructor I put some data in the list box:
public MainWindow( )
{
InitializeComponent( );
this.x_list.ItemsSource = Enumerable.Range( 0, 100 );
}
next, I'm implementing a findVisualItem - to find the root element of the data template. I've made this function a little generic, so it get a predicate p which identify whether this is the element I want to find:
private DependencyObject findVisualItem( DependencyObject el, Predicate<DependencyObject> p )
{
DependencyObject found = null;
if( p(el) ) {
found = el;
}
else {
int count = VisualTreeHelper.GetChildrenCount( el );
for( int i=0; i<count; ++i ) {
DependencyObject c = VisualTreeHelper.GetChild( el, i );
found = findVisualItem( c, p );
if( found != null )
break;
}
}
return found;
}
I'll use the following predicate, which returns true if the element I'm looking for is a border, and its name is "x". You should modify this predicate to match your root element of your ItemTemplate.
findVisualItem(
x_list,
el => { return ( el is Border ) ? ( (FrameworkElement)el ).Name == "x" : false; }
);
Finally, the lbHeight property:
public double lbHeight
{
get {
FrameworkElement item = findVisualItem(
x_list,
el => { return ( el is Border ) ? ( (FrameworkElement)el ).Name == "x" : false; }
) as FrameworkElement;
if( item != null ) {
double h = item.ActualHeight + item.Margin.Top + item.Margin.Bottom;
return h * 12;
}
else {
return 120;
}
}
}
I've also made the Window implementing INotifyPropertyChanged, and when the items of the list box were loaded (Loaded event of ListBox) I fired a PropertyChanged event for the 'lbHeight' property. At some point it was necessary, but at the end WPF fetched the lbHeight property when I already have a rendered Item.
It is possible your Items aren't identical in Height, in which case you'll have to sum all the Items in the VirtualizedStackPanel. If you have a Horizontal scroll bar, you'll have to consider it for the total height of course. But this is the overall idea. It is only 3 hours since you published your question - I hope someone will come with a simpler answer.
This is done by setting parent control Height property to Auto, without setting any size to the Listbox itself (or also setting to Auto).
To limit the list size you should also specify MaxHeight Property
Microsoft I think this may be a bug. Have a ListView GridView to display a single record.
Naturally the down arrow key navigates through the ListViewItems. That is the behavior I expect. In XAML the ItemsSource and SelectedIndex is bound. The behavior I want is the right and left arrow keys to navigate to the next and prior record.
The way I achieved this is to handle ListView PreviewKeyDown Event. The problem I had is the SelectedIndex is reset to -1 when the ItemSource is refreshed and I want it to stay on the old SelectedIndex. So I had to manually reset the SelectedIndex as shown below.
Here is my problem: After AND ONLY DIRECTLY AFTER a left arrow or right arrow key the up and down keys do not pass the correct value for SelectedIndex.
For example SelectedIndex = 4 then right arrow and then down key the value passed to SelectedIndex is 0 and the value passed should be 4. The ListView is visually on SelectedIndex 4 so it is like it knows the SelectedIndex is 4 but it also does not know. If I do not change the ItemsSource the up and down arrow keys work correctly. I tried KeyDown and KeyUp events but in the case those events just don't fire at all in the fail condition described above.
WPF 4.0 Visual Studio 2010.
private void lvCurDocFields_PreviewKeyDown(object sender, KeyEventArgs e)
{
Debug.WriteLine("lvCurDocFields_PreviewKeyDown " + e.Key.ToString());
e.Handled = false;
Int32 lastIndex;
switch (e.Key) // e.KeyCode
{
case Key.Right:
lastIndex = lvCurDocFields.SelectedIndex;
App.StaticGabeLib.Search.SelectedDocIndex++;
if (lvCurDocFields.SelectedIndex != lastIndex)
{
lvCurDocFields.SelectedIndex = lastIndex;
}
e.Handled = true;
break;
case Key.Left:
lastIndex = lvCurDocFields.SelectedIndex;
App.StaticGabeLib.Search.SelectedDocIndex--;
if (lvCurDocFields.SelectedIndex != lastIndex)
{
lvCurDocFields.SelectedIndex = lastIndex;
}
e.Handled = true;
break;
}
base.OnPreviewKeyDown(e);
}
Normally, I store a reference to the item that was selected, update the list, and re-select it afterwards. ListIndex isn't a reliable way to maintain a selection if things are inserted or deleted from the list.
if(MyListControl.Items.Contains(PreviouslySelectedItem)) MyListControl.SelectedItem = PreviouslySelectedItem;
I want to add a new line in my datagrid when I press 'TAB' key on last cell of the datagrid.
I am using MVVM pattern to do this. I have came with a solution, I assinged Tab key to the Input binding of the datagrid:
<DataGrid.InputBindings>
<KeyBinding Command="{Binding Path=InsertNewLineCommand}" Key="Tab"></KeyBinding>
</DataGrid.InputBindings>
And added following code to InsertNewLineCommand:
private void ExecuteInsertNewLineCommand()
{
//Checked is SelectedCell[0] at last cell of the datagrid
{
InsertNewLine();
}
}
But the problem is ON ADDING KEYBINDING='TAB' MY NORMAL TAB FEATURE ON THE GRID DISABLES (MOVING TO NEXT CELL AND SO...)
Just determine if you are on the last column then execute your command.
I am using PreviewKeyDown, so I could test the logic, but you can put that in your executeCommand method. Anyway, this should get you started:
<DataGrid PreviewKeyDown="DataGrid_PreviewKeyDown" SelectionUnit="Cell" ....
private void DataGrid_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (!Keyboard.IsKeyDown(Key.Tab)) return;
var dataGrid = (DataGrid) sender;
var current = dataGrid.Columns.IndexOf(dataGrid.CurrentColumn);
var last = dataGrid.Columns.Count - 1;
if (current == last)
ExecuteInsertNewLineCommand();
}