WPF - Setting tab order on large number of controls - c#

So I have a large amount of controls (textboxes) as you can see below, but there are around 30 rows of this. These are loaded using arrays, and each column represents an array. So when I hit tab in a textbox, instead of tabbing horizontally, it tabs vertically instead.
Is there a way to set the tab order so it will tab horizontally, aside from changing the way the controls are loaded?
Another quirk is that when leaving one textbox, instead of focusing the next, it just kind of highlights the textbox, and I have to tab a second time to get inside the next textbox.
EDIT:
Main view (lots of code has been omitted, I'm pretty sure nothing has been left out that needs to be here)
<ListBox ItemsSource="{Binding Items}" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
ItemsView
<UserControl>
<UserControl.Resources>
<DataTemplate DataType="{x:Type vm:Item}">
<views:ItemView/>
</DataTemplate>
</UserControl.Resources>
<StackPanel>
<ContentControl Content="{Binding item <!-- about 30 different items here, omitted for readability -->}" />
</StackPanel>
</UserControl>
ItemView
<UserControl ... IsTabStop="False">
<TextBox Text="{Binding Value}" />
</UserControl>
The ItemView is nested in the ItemsView, which is nested in the MainView. Since the textboxes are generated based on the array values, I can't easily set the TabIndex property unless there is a way I don't know about (I am pretty new at WPF).

The TabIndex property provides a way to control the tab order independently of the order controls are loaded.
Usage example:
<Grid>
<TextBox TabIndex="2" /><!-- will receive focus second -->
<TextBox TabIndex="1" /><!-- will receive focus first-->
</Grid>
I would guess the unwanted focusing you are seeing is due to a parent UserControl that your TextBoxes are placed in.
If this is the case, you could prevent that by setting IsTabStop="false" on that parent control.
For example:
<UserControl .... IsTabStop="False">
<Grid>
<!-- other graphics -->
<TextBox TabIndex="1" />
</Grid>
</UserControl>
Using a view model to populate the data
public class CellViewModel
{
public double Value { get; set; }
public int TabIndex { get; set; }
}
public IEnumerable<IEnumerable<CellViewModel>> GetMatrix(
List<List<double>> matrixValues)
{
var columnCount = matrixValues.Count;
return matrixValues
.Select((x, i) => GetColumn(x, columnCount, i));
}
public IEnumerable<CellViewModel> GetColumn(
List<double> columnValues,
int columnCount,
int columnIndex)
{
return columnValues
.Select((x, i) =>
new CellViewModel { Value = x, TabIndex = columnIndex + columnCount * i });
}
Your ItemsSource for your ListBox (which you've now changed to ItemsControl) should be a new Matrix property, which you populate using GetMatrix().
In your ItemView, you would want something like this:
<UserControl ... IsTabStop="False">
<TextBox Text="{Binding Value}" TabIndex="{Binding TabIndex}" />
</UserControl>

Related

Dynamic text boxes binding

I am trying to make a form of sorts where the user will have the option to press a button to dynamically add more text boxes, with each text box to contain the path of a directory to exclude from a search. This is relatively trivial using code-behind, but my problem is doing it in proper MVVM.
The general markup for the structure is:
<ScrollViewer >
<StackPanel>
<DockPanel LastChildFill="False">
<TextBox DockPanel.Dock="Left"/>
<Button DockPanel.Dock="Right"
Content="+"/>
</DockPanel>
</StackPanel>
</ScrollViewer>
Clicking the button will add a new DockPanel with a TextBox and Button to the StackPanel. All buttons but the bottom-most will change to a minus sign. How can I somehow bind to the text of all the text boxes?
As an aside, once/if I get this working, would it be better to make it into its own component?
Quick pseudo-code to get you started:
cs (view model):
// INotifyPropertyChanged if you need, as well as full-properties with notification
public class Item
{
public string Text {get; set;}
}
public ObservableCollection<Item> Items { get; } = new ObservableCollection<Item>();
void OnCommandAdd() => Items.Add(new Item());
xaml (view):
<ListBox ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Text}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The idea is to use control able to display list (e.g. ItemsControl) and collection of items in view model. Adding new element to view is done by adding item to that collection.
All TextBoxes will have Text bound to corresponding property of item.

Why does my dropdown feel so clunky?

I have a XAML UserControl embedded in a WinForms/WPF Interop ElementHost control. The control is pretty simple - it's just a dropdown with a button - here's the entire markup:
<UserControl x:Class="Rubberduck.UI.FindSymbol.FindSymbolControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Rubberduck.UI.FindSymbol"
mc:Ignorable="d"
d:DesignHeight="27" d:DesignWidth="270">
<UserControl.Resources>
<local:DeclarationImageConverter x:Key="DeclarationImageConverter" />
</UserControl.Resources>
<UserControl.CommandBindings>
<CommandBinding Command="local:FindSymbolControl.GoCommand"
Executed="CommandBinding_OnExecuted"
CanExecute="CommandBinding_OnCanExecute"/>
</UserControl.CommandBindings>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="32" />
</Grid.ColumnDefinitions>
<ComboBox IsEditable="True"
ItemsSource="{Binding MatchResults}"
SelectedItem="{Binding SelectedItem, UpdateSourceTrigger=PropertyChanged}"
Text="{Binding SearchString, UpdateSourceTrigger=PropertyChanged}"
IsTextSearchCaseSensitive="False"
IsTextSearchEnabled="True"
TextSearch.TextPath="IdentifierName">
<ComboBox.ItemTemplate>
<DataTemplate DataType="local:SearchResult">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Image Height="16" Width="16" Margin="2,0,2,0" Source="{Binding Declaration, Converter={StaticResource DeclarationImageConverter}}" />
<TextBlock Margin="2,0,2,0" Text="{Binding IdentifierName}" FontWeight="Bold" MinWidth="140" />
<TextBlock Margin="2,0,2,0" Text="{Binding Location}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button Grid.Column="1"
Command="local:FindSymbolControl.GoCommand">
<Image Height="16" Source="pack://application:,,,/Rubberduck;component/Resources/arrow.png" />
</Button>
</Grid>
</UserControl>
The problem is that it doesn't work reliably, and far from instinctively.
If I type something in the box that actually matches an item, nothing happens until I manually select that item in the dropdown. Like here, I typed "sleepD", the box autocompleted to "sleepDelay", but the command is still disabled:
Once I've selected the item in the dropdown, the command button gets enabled as expected (although the image on the button doesn't show up grayed-out when the button is disabled, so it's not exactly as obvious as I intended it to be).
(the screenshot isn't really showing it, but there's only 1 match for that search)
If I click the button at that point, it works as expected. The problem is that if I make a new selection from the dropdown after that, the text box gets cleared instead of displaying the item I selected, and there's a weird delay during which the box is displaying what appears to be selected whitespace - this only seems to happen when the previous selection was made after selecting a value in the dropdown while the search text matches multiple entries, like "Sleep" above.
After the box got cleared, I can make a new selection from the dropdown and it will work as expected (except the VBE won't actually activate the CodePane I'm setting the selection to, but that's a separate issue).
The command implementation simply raises a Navigate event that passes a Declaration to the code that owns the VM instance.
The Search method, for which I need to add a .Take(50) after the .Select, to limit the number of returned results and perhaps reduce the lag a bit:
private void Search(string value)
{
var lower = value.ToLowerInvariant();
var results = _declarations.Where(
declaration => declaration.IdentifierName.ToLowerInvariant().Contains(lower))
.OrderBy(declaration => declaration.IdentifierName.ToLowerInvariant())
.Select(declaration => new SearchResult(declaration));
MatchResults = new ObservableCollection<SearchResult>(results);
}
private string _searchString;
public string SearchString
{
get { return _searchString; }
set
{
_searchString = value;
Search(value);
}
}
private SearchResult _selectedItem;
public SearchResult SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
OnPropertyChanged();
}
}
private ObservableCollection<SearchResult> _matchResults;
public ObservableCollection<SearchResult> MatchResults
{
get { return _matchResults; }
set { _matchResults = value; OnPropertyChanged(); }
}
}
There's also an IValueConverter involved, that takes the Declaration in the SearchResult and switches on the declaration's DeclarationType enum to return a pack uri that points to the .png image to use in the dropdown list.
Aaah found it. It was all in the XAML.
Right here:
Text="{Binding SearchString, UpdateSourceTrigger=PropertyChanged}"
That line doesn't belong there; binding the TextSearch.Text property instead...
TextSearch.Text="{Binding SearchString, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged}"
Makes it all work as intended. No glitch, no lag. Well there is a lag when I first drop the dropdown, but that's another issue.
Lesson learned: when TextSearch is enabled on an editable combobox, don't bind the Text property, unless you want weird behavior.

How to display a "more item" in an ItemsControl when the number of elements is bigger than n?

I have an ItemsControl that displays tiles in a Windows 8 manner with 2 columns and 4 rows. Each tile is clickable and triggers a command that will load the selected item in another view.
My problem starts here: my bound IList<> can contain more than 8 elements at the time, but must display no more than 8 tiles.
What I am trying to achieve is to create a different type of tile (linked to another command) that will appear (ex: using a Converter) only when my IList<> is bigger than 8. Please check the drawing below to understand my aim.
So far I could limit the number of elements retrieved in the IList<> container to 7 whenever it is bigger than 8, but adding the "special" 8th element is still a mystery for me.
I have used CompositeCollection to solve the problem this enables multiple collections and items to be displayed as a single list. more on CompositeCollection
here is a sample
xaml
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="2" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type sys:Int32}">
<Border Margin="4"
Background="LightSkyBlue">
<TextBlock Text="{Binding}" FontSize="15"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
</DataTemplate>
<DataTemplate DataType="{x:Type sys:String}">
<Border Margin="4"
Background="MediumPurple">
<TextBlock Text="{Binding}" FontWeight="SemiBold"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
note that I have defined data templates for two different types i.e. int and string which will help me render the same accordingly
public ViewModel()
{
IEnumerable<int> originalData = Enumerable.Range(1, 12);
Items = new CompositeCollection();
Items.Add(new CollectionContainer() { Collection = originalData.Take(originalData.Count() > 8 ? 7 : 8) });
if (originalData.Count() > 8)
Items.Add(originalData.Count() - 7 + " more");
}
public CompositeCollection Items { get; set; }
whole idea is to limit the number of elements in the primary collection and add an extra element to the collection of different type eg original list is int and extra is a string
so item control will render all the elements in the collection and we can control the appearance based on the data type
you may also make use of Attached Properties or Converters to simplify this or perform more sophisticated functions.
result

Storing properties of objects in C#/WPF

I'm allowing user to drag/drop some objects from a toolbox and of course each object has a unique id. As soon as object is used, let's say placed on a grid or canvas, I need to show its properties so I need an array of objects where each object can hold its own properties.
Can you give me some advice and direction on how to implement a class to handle multiple objects while each object can hold on to let's say 10 properties?
The best solution is to use a PropertyGrid control; your application looks similar to Visual Studio and your implementation will be similar to that.
Have a look at this SO question for available PropertyGrid options you have -
Is there a Property Dialog control that i can use in my WPF App?
Now you can define a class for each control and declare normal CLR properties for that control; properties you don't want to display in PropertyGrid can be marked with BrowsableAttribute and PropertyGrid will honor that.
In case you want more control over what properties are displayed, you can create your own custom attribute and modify PropertyGrid implementation to use that attribute and display properties marked with this attribute.
Can you give me some advice and direction on how to implement a class
to handle multiple objects while each object can hold on to let's say
10 properties?
There is no need for you to implement such a class. The way I would handle this problem would be to have a common base class for all the objects in the toolbox (ToolboxItem for example) which only exposes properties and functionality common to all items in the toolbox.
public abstract class ToolboxItem
{
public string Name { get; set; }
public Point Position { get; set; }
}
You can then derive your specific items from this class E.G. TextToolboxItem and RectangleToolboxItem (or whatever you want). The derived classes can then expose only the properties they require.
public class TextToolboxItem : ToolboxItem
{
public string Text { get; set; }
}
public class RectangleToolboxItem : ToolboxItem
{
public Rect Bounds { get; set; }
}
To store these you could just use a generic collection such as:
ObservableCollection<ToolboxItem> items = new ObservableCollection<ToolboxItems>();
As long as the items derive from ToolboxItem they can all be held within the single collection and the individual properties can all be bound to using WPF's data binding features.
You can then create and expose the data in the following way:
public partial class MainWindow : Window
{
private ObservableCollection<ToolboxItem> items;
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
items = new ObservableCollection<ToolboxItem>
{
new TextToolboxItem { Name = "primaryText",
Text = "Hello world",
Position = new Point(40, 130) },
new TextToolboxItem { Name = "secondaryText",
Text = "Hello world (again)",
Position = new Point(200, 30) },
new RectangleToolboxItem { Position = new Point(50,300),
Name = "Rect1",
Bounds = new Rect(0, 0, 150, 85) },
};
}
public ObservableCollection<ToolboxItem> Items { get { return items; } }
}
To display this information in the user interface I would do the following:
Use a grid to split the view into two sections. The first is where the properties of the selected item will be displayed and the second displays the 'design surface'
Use a ContentPresenter to display the properties of the selected item.
Use a ListBox with a custom ItemsPanel and ItemContainerStyle to 'draw' your items onto the design surface.
Use a DataTemplate to tell WPF how to render each item in both the 'property grid' and the 'design surface' (This post describes how to use a different DataTemplate for different objects).
The xaml required to achieve this is shown below:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:this="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="7*" />
</Grid.ColumnDefinitions>
<ContentPresenter Content="{Binding ElementName=listBox, Path=SelectedItem}"
Margin="5">
<ContentPresenter.Resources>
<DataTemplate DataType="{x:Type this:TextToolboxItem}">
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Position}"/>
<TextBlock Text="{Binding Text}"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type this:RectangleToolboxItem}">
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Position}"/>
<TextBlock Text="{Binding Bounds}"/>
</StackPanel>
</DataTemplate>
</ContentPresenter.Resources>
</ContentPresenter>
<ListBox x:Name="listBox" Grid.Column="1"
Margin="5" ItemsSource="{Binding Items}">
<ListBox.Resources>
<DataTemplate DataType="{x:Type this:TextToolboxItem}">
<TextBox Text="{Binding Text}"
Margin="10"/>
</DataTemplate>
<DataTemplate DataType="{x:Type this:RectangleToolboxItem}">
<Rectangle Width="{Binding Bounds.Width}"
Height="{Binding Bounds.Height}"
Stroke="DarkRed" Fill="Pink"/>
</DataTemplate>
</ListBox.Resources>
<ListBox.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Left" Value="{Binding Position.X}"/>
<Setter Property="Canvas.Top" Value="{Binding Position.Y}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</Grid>
The end result looks like this:
Notice that the properties of the selected item are shown in the left hand section of the window.
Now this solution is currently very crude but does demonstrate a starting point for you to develop this further. Ideas for improvement include:
Re-factoring the code into a viewModel so that it is MVVM compliant.
Handling drag and drop of the items on the 'design surface'.
Changing the `ContentPresenter' for a property grid to give you much richer support for displaying and editing the properties of the selected object.

Dynamically build a selection list from a list with an unknown number of members

What I am trying to do is create a silevrlight popup that displays images to a user and lets them select a radio control underneath to determine which option they have selected. I have an object like this:
public class ConfigImage
{
public int ConfigID { get; set; }
public string ConfigName { get; set; }
public string ImagePath { get; set; }
}
The code returns a list of ConfigImage with an unknown number of members. I am trying to use a grid to display the images to the user, so I dynamically add columns based on the number of members in the list. I expect anywhere from 2-5 members to be in the list. What I am having the problem with is trying to dynamically add the image and radio controls. I cannot seem to find an example anywhere of this. I tried to add controls using code such as this:
LayoutRoot.Children.Add(new Label);
but then have no idea how to set properties on the new Label control. I should know this, but I am drawing blank and cannot seem to find an example of it.
Help would be much appreciated!
If you absolutely have to add the controls in code, you will need to have a reference to the object in order to set properties on it:
Label label = new Label();
label.Content = "text";
label.Width = 10;
LayoutRoot.Children.Add(label);
Alternatively, you can use initializers:
LayoutRoot.Children.Add(new Label()
{
Content = "text",
Width = 10
});
As BrokenGlass said, you can probably do this completely in xaml, though.
Edit: To illustrate the xaml-only approach using BrokenGlass's suggestion of ItemsControl:
<ItemsControl x:Name="ConfigImagesItemsControl" ItemsSource="MyConfigImagesList">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image Grid.Row="0" Source="{Binding ImagePath}" />
<RadioButton Grid.Row="1" Content="{Binding ConfigName}" GroupName="ConfigImagesGroup" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
just use any of the list based UI elements in Silverlight - those would allow you to data bind to an observable collection that you can update at runtime, simplest one would be ItemsControl - what markup you use for each item in the collection you can completely control in XAML.

Categories

Resources