I have a WPF application with a DataGrid (from WPF Toolkit) control. The ItemsSouce property is bound to an ObservableCollection in my ViewModel.
The data grid has a column with a TextBox in it:
<dg:DataGrid.Columns>
<dg:DataGridTemplateColumn Header="Name">
<dg:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding UserName}" Width="300"/>
</DataTemplate>
</dg:DataGridTemplateColumn.CellTemplate>
</dg:DataGridTemplateColumn>
...
I also have an "Add" button to create a new user. When I click this, a new row is created. I would like, however, for the above textbox to get the input focus (on the new row of course). I have looked at:
WPF MVVM Focus Field on Load
WPF-MVVM: Setting UI control focus from ViewModel
How to set focus to textbox using MVVM?
Set focus on textbox in WPF from view model (C#)
But all of them seem to rely on same variation of an "ElementName" binding and none look like they would work in an ItemsControl. What is the correct way to get this behavior?
One way I believe you can do this is to have a trigger on the textbox that handles on Loaded and have it set focus. Something like this
<dg:DataGridTemplateColumn Header="Name">
<dg:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" Text="{Binding UserName}" Width="300">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<local:SetFocusTrigger/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
</DataTemplate>
</dg:DataGridTemplateColumn.CellTemplate>
</dg:DataGridTemplateColumn>
And the SetFocusTrigger class is :-
public class SetFocusTrigger : TargetedTriggerAction<Control>
{
protected override void Invoke(object parameter)
{
if (Target == null) return;
Target.Focus();
}
}
Note I haven't tried this out.
I tried to do in pure XAML
<DataGrid CanUserAddRows="True"
ItemsSource="{Binding List}"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Name">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"
Width="300" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Text="{Binding Name}"
FocusManager.FocusedElement="{Binding RelativeSource={RelativeSource Self}}"
Width="300" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
I added two templates 1 for normal and other for edit mode. so once cell enter edit mode it will focus the text box.
if you always want the text box to be visible then it would have a different behavior as the new row is binded to placeholder and the value is text box will persist and move over to every new row but the real row.
ListView Solution (MVVM)
sample XAML
<StackPanel>
<Button Command="{Binding AddItem}"
Content="Add Item"/>
<ListView ItemsSource="{Binding List}"
Grid.IsSharedSizeScope="True">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="name" />
<!--other columns-->
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBox Text="{Binding Name}"
FocusManager.FocusedElement="{Binding RelativeSource={RelativeSource Self}}" />
<TextBox Text="other column" Grid.Column="1"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
sample VM
public ViewModel()
{
AddItem = new SimpleCommand(i => List.Add(new Person()));
List = new ObservableCollection<object>(new[] { new Person() { Name = "a person"} });
}
public ObservableCollection<object> List { get; set; }
class Person
{
public string Name { get; set; }
}
public ICommand AddItem { get; set; }
so upon adding new item the new row will be created by means of adding a new Person to the List and will be displayed in view with the focus on the text box.
run the sample above and see the behavior, no code behind involved.
Related
UWP Application (Important because there is no AncestorType)
I can't bind command (neither other values) of the ViewModel from a DataGridTemplateColumn.
Here is my current code (i have tried, literally everything)
<controls:DataGrid
x:Name="DataGrid"
Grid.Row="2"
Height="Auto"
Margin="12"
AutoGenerateColumns="False"
HorizontalContentAlignment="Center"
ItemsSource="{Binding ProviderOrders}">
<controls:DataGrid.Columns>
<controls:DataGridTemplateColumn Header="Actions" Width="*">
<controls:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="Modifier" Command="{Binding DataContext.EditOrderCommand, RelativeSource={RelativeSource Mode=Self}}" CommandParameter="{Binding}" Style="{StaticResource PrimaryButton}"/>
</DataTemplate>
</controls:DataGridTemplateColumn.CellTemplate>
</controls:DataGridTemplateColumn>
</controls:DataGrid.Columns>
</controls:DataGrid>
I have also tried
<Button Content="Modifier" Command="{Binding ElementName=DataGrid, Path=DataContext.EditOrderCommand}" CommandParameter="{Binding}" Style="{StaticResource PrimaryButton}"/>
There is no error but my Command is not runned and my command is working if i move the Button outside the DataGrid..
The DataGridTemplateColumn DataContext is the ProviderOrder object and so i need to access of the ViewModel (which is obviously not accessible from the ProviderOrder object)
Thanks in advance :)
Great question, this known issue in DataGrid control. Currently, there is a workaroung for this scenario that bind command for button in CellTemplate, please add the command in the datasouce.
public class Item
{
public string ID { get; set; }
public ICommand BtnCommand
{
get
{
return new CommadEventHandler<Item>((s) => BtnClick(s));
}
}
private void BtnClick(Item s)
{
}
}
Xaml Code
<controls:DataGridTemplateColumn>
<controls:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button
Command="{Binding BtnCommand}"
Content="Click"
/>
</DataTemplate>
</controls:DataGridTemplateColumn.CellTemplate>
</controls:DataGridTemplateColumn>
Update
If not specific DataGrid, you could use listview to replace, and Binding ElementName will work.
In a WPF DataGrid, I want to show a validation result in a small box inside the cell.
I managed to do so for a single column by binding to the Validation.Errors data structure (see the code below).
This is what I got and it's pretty close to the desired outcome; now I want to implement it for all the columns.
The problem
In order to make the solution reusable over multiple columns I tried to move it into a ControlTemplate. I couldn't find a way to establish the binding of Validation.Errors again from inside the control template (See the code below). As a result, the red label is always empty.
The working, single-column solution
The working solution is based on following code:
<DataGrid ItemsSource="{Binding People}" AutoGenerateColumns="False" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Name">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid>
<Label x:Name="x" Content="{Binding Name}"/>
<Label Padding="2" HorizontalAlignment="Right" VerticalAlignment="Top" Height="15" Width="44" FontSize="8" Foreground="White" Background="Red"
Content="{Binding ElementName='x', Path='(Validation.Errors)[0].ErrorContent'}"/>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
No need to read it all: here are the relevant parts
It works by binding the Label "x" to the Name property of my example datacontext.
<Label x:Name="x" Content="{Binding Name}"/>
Then, the error label in turn binds to the former Label (via its name) and gets the Validation.Errors information (graphics formats removed here for clearness).
<Label Content="{Binding ElementName='x', Path='(Validation.Errors)[0].ErrorContent'}"/>
This proves that the result is achieveable, but this solution cannot be reused over multiple columns without repeating it over and over again.
Wrapping attempt
In order to have a reusable template, i tried to wrap all my cell contorls (label x and label with x's errors) into a ControlTemplate; it will be used by a Label component that is what i'll actually have on the grid.
The wrapping code is this (bewlow there is the complete code):
<Label Content="{Binding Name}">
<Label.Template>
<ControlTemplate TargetType="Label">
//my controls
</ControlTemplate>
</Label.Template>
</Label>
About "my contols"
I had to change the line:
<Label x:Name="x" Content="{Binding Name}"/>
to this:
<Label x:Name="x" Content="{TemplateBinding Content}"/>
But the Label dedicated to the errors doesnt work anymore (graphics configuration removed):
I can guess that it doesn't work because only the content property is trasfered form the templated label to the inner label x; the content and not the entire 'state' of the property including the validation errors collection. But how can I access those errors then?
Code
<Window.DataContext>
<local:ViewModel/>
</Window.DataContext>
<DataGrid ItemsSource="{Binding People}" AutoGenerateColumns="False" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Name">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Label Content="{Binding Name}">
<Label.Template>
<ControlTemplate TargetType="Label">
<Grid>
<Label x:Name="x" Content="{TemplateBinding Content}"/>
<Label Padding="2" HorizontalAlignment="Right" VerticalAlignment="Top" Height="15" Width="44" FontSize="8" Foreground="White" Background="Red"
Content="{Binding ElementName='x', Path='(Validation.Errors)[0].ErrorContent'}"/>
</Grid>
</ControlTemplate>
</Label.Template>
</Label>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
DataContext
public class ViewModel
{
public ObservableCollection<Person> People { get; } = new ObservableCollection<Person>() { new Person { Name = "Alan" } };
}
public class Person: INotifyDataErrorInfo
{
public string Name { get; set; }
public bool HasErrors => true;
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public IEnumerable GetErrors(string propertyName)
{
yield return "Some error";
}
}
Instead of Binding to Validation.Errors of Label 'x', you can refer to Validation.Errors of the TemplatedParent, i.e. Main Label.
I was able to extract the ControlTemplate to window resource, and use this resource as Label Template, so we can reuse this template.
<Window.Resources>
<ControlTemplate TargetType="Label" x:Key="Lbl">
<Grid>
<Label x:Name="x" Content="{TemplateBinding Content}"/>
<Label Padding="2" HorizontalAlignment="Right" VerticalAlignment="Top" Height="15" Width="44" FontSize="8" Foreground="White" Background="Red"
Content="{Binding (Validation.Errors)[0].ErrorContent, RelativeSource={RelativeSource TemplatedParent}}"/>
</Grid>
</ControlTemplate>
</Window.Resources>
<DataGrid ItemsSource="{Binding People}" AutoGenerateColumns="False" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Name">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Label Content="{Binding Name}"
Template="{StaticResource Lbl}">
</Label>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
I have a listbox in the dataTemplate of an other listbox. I can get selected item of the outer listbox, but I've been trying to get selected item of the inner listbox (name: "ListBoxLetter"), no way ..
Here is my xaml :
<ListBox x:Name="ListBoxOut" ItemsSource="{Binding Letters}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<StackPanel Width="500" Height="60" Background="#16C8DB">
<TextBlock Text="{Binding Date}" />
</StackPanel>
<ListBox x:Name="ListBoxLetter" ItemsSource="{Binding CourriersListe}" SelectedItem="{Binding Selection, Mode=TwoWay}" >
<Interactivity:Interaction.Triggers>
<Interactivity:EventTrigger EventName="SelectionChanged" >
<Command:EventToCommand Command="{Binding SelectionCommand}" CommandParameter="{Binding Selection}" />
</Interactivity:EventTrigger>
</Interactivity:Interaction.Triggers>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Date}" />
<TextBlock Text="{Binding Name}"/>
</StackPanel>
<TextBlock Text="{Binding Title}" />
</StackPanel>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>`
"Courriers" is an object of the following class :
public class MyClass
{
public DateTime Date { get; set; }
public List<LetterInfos> CourriersListe { get; set; }
}
And a LetterInfos has a Date, Name and Title.
Selection is a LetterInfos object, and I want to get it when I click on it in my list.
I'm working with mvvm light, so in my ViewModel constructor I have this :
SelectionCommand = new RelayCommand<LetterInfos>(OnSelectionElement);
I tried to move the interactivity paragraph in the outer listbox, but I can only get the MyClass selected item, and I want to select a LetterInfos item..
Anyone could help me ? Thanks !!
After many researches, I think that's not possible to get selectedItem in this situation.
But I found a good solution to this problem : the LongListSelector.
Even if its selectedItem is not bindable, there is a class to add that can make it bindable (to the parameter of a command) : Trouble binding LongListSelector.SelectedItem to MVVM property
I’ve got a datagrid with one column displaying a combobox. At present the new row is shown underneath existing rows – as expected.
<grid>
<DockPanel Grid.Column="0" Grid.Row="0">
<TextBlock DockPanel.Dock="Top" Text="Role Groups"/>
<DataGrid DockPanel.Dock="Bottom" Name="dgRoleGroups" AutoGenerateColumns="False" CanUserAddRows="True" CanUserDeleteRows="True" HorizontalAlignment="Left" ItemsSource="{Binding ListSecurityUserRoleGroup}">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Role Group" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding ListSecurityRoleGroup,
RelativeSource={RelativeSource AncestorType=UserControl}}"
DisplayMemberPath="Description" SelectedValuePath="ID"
SelectedValue="{Binding RoleGroupID}”/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</Grid>
Where ListSecurityUserRoleGroup is an ObservableCollection of:
public class tbl_SecurityUserRoleGroup_Row
{
public int UserID { get; set; }
public int RoleGroupID { get; set; }
}
And ListSecurityRoleGroup is a list of:
public class tbl_Security_RoleGroup_Row
{
public int ID { get; set; }
public string PublicID { get; set; }
public string Description { get; set; }
}
In the code behind I’ve got:
dgRoleGroups.DataContext = ListSecurityUserRoleGroup;
dgRoleGroups.ItemsSource = ListSecurityUserRoleGroup;
The pic below shows that the binding for the first row is working; and I’ve got a new row and can pick a value for that.
However, I then cannot get another new row. This is the problem I’m trying to solve.
From reading other posts, I suspect I’m missing something in the realm of IEditableObject, INotifiyProperyChanged or because there is only one column in this datagrid, maybe need to trigger something from the combobox SelectedChanged – like check to see if a blank row is visible and if not, create one ?
I've not found a post matching my issue but i'm sure it's there...
There may be other solutions that do not involve datagrids however once I’ve got this working, my next task is a datagrid containing 2 columns of comboboxes, which will need to work there.
You just need to add an edit template :
<Grid>
<DockPanel Grid.Column="0" Grid.Row="0">
<TextBlock DockPanel.Dock="Top" Text="Role Groups"/>
<DataGrid DockPanel.Dock="Bottom" Name="dgRoleGroups" AutoGenerateColumns="False"
CanUserAddRows="True" CanUserDeleteRows="True"
HorizontalAlignment="Left" ItemsSource="{Binding ListSecurityUserRoleGroup}">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Role Group" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding ListSecurityRoleGroup,
RelativeSource={RelativeSource AncestorType=UserControl}}" SelectedValue="{Binding RoleGroupID,UpdateSourceTrigger=PropertyChanged}"
DisplayMemberPath="Description" SelectedValuePath="ID" IsHitTestVisible="False">
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding ListSecurityRoleGroup,
RelativeSource={RelativeSource AncestorType=UserControl}}"
DisplayMemberPath="Description" SelectedValuePath="ID"
SelectedValue="{Binding RoleGroupID,UpdateSourceTrigger=PropertyChanged}"
/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</Grid>
You can also modify the combobox template to look like a textblock :
<ComboBox.Template>
<ControlTemplate>
<TextBlock Text="{Binding SelectedItem.Description,RelativeSource={RelativeSource Mode=TemplatedParent}}"></TextBlock>
</ControlTemplate>
</ComboBox.Template>
I have 2 textbox, each in a different listview. First textbox is supposed to show data from a xml file. So when i click on the textbox, the data in the first textbox will show on the second textbox. I did this by doing a very big round about, getting the specific object when i click it and append to another listview. Is there a shorter way to do this through binding by element name in the xaml? My elementName in textbox1 will be the name for textbox2. I try doing it, but I am not sure what my path should be?
Sorry for not including my xaml.
<Window x:Class="GridViewTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase"
xmlns:local="clr-namespace:GridViewTest"
Title="MainWindow" mc:Ignorable="d" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" d:DesignHeight="541" d:DesignWidth="858" SizeToContent="WidthAndHeight">
<Window.Resources>
<local:PacketList x:Key="PacketList"/>
<local:BindableSelectionTextBox x:Key="BindableSelectionTextBox"/>
</Window.Resources>
<Grid Height="500" Width="798">
<Grid.RowDefinitions>
<RowDefinition Height="142*" />
<RowDefinition Height="145*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="234*" />
<ColumnDefinition Width="233*" />
</Grid.ColumnDefinitions>
<ListView ItemsSource="{Binding}" x:Name="lvItems" Grid.RowSpan="2" Grid.ColumnSpan="2">
<ListView.View>
<GridView AllowsColumnReorder="True">
<GridViewColumn Header="Header" Width="200">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Grid>
<TextBox Name ="A" Tag="Header" Text="{Binding SelectedText, Path=headerObj.headervalue}" PreviewMouseLeftButtonUp="Handle_Click"
IsReadOnly="True" BorderThickness="0" >
</TextBox>
</Grid>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
<ListView Margin="0,245,0,8" Grid.ColumnSpan="2" Grid.RowSpan="2" >
<TextBox Name="headText" Text="{Binding SelectedText,ElementName=A}"/>
</ListView>
</Grid>
Firstly let us have some education on NameScoping in WPF. In WPF any Bindings within Templates are scoped to that Template only. Also any element named within a template wont be available for Binding.ElementName reference outside the template.
So in your case TextBox A cannot be referred by TextBox headText as textbox A is name-scoped under GridViewColumn.CellTemplate.
Also why is headText textbox under a ListView? ItemsControls like ListBox, ListView, DataGrid should not be used as panels or containers to host single elements. Their intention is to show multiple items. Use Panels or ContentControl instead.
<Grid Margin="0,245,0,8" Grid.ColumnSpan="2" Grid.RowSpan="2" >
<TextBox Name="headText" Text="{Binding SelectedText,ElementName=A}"/>
</Grid>
OR
<ContentControl Margin="0,245,0,8" Grid.ColumnSpan="2" Grid.RowSpan="2" >
<TextBox Name="headText" Text="{Binding SelectedText,ElementName=A}"/>
</ContentControl>
Now to synchronize selection between two textboxes use the following trick...
XAML
<TextBox Name="SelectionSource"
Tag="{Binding ElementName=SelectionTarget}"
SelectionChanged="SelectionSource_SelectionChanged" />
<TextBox Name="SelectionTarget"
Text="{Binding SelectedText, ElementName=SelectionSource,
Mode=TwoWay, UpdateSourceTrigger=Explicit}" />
Code Behind ...
private void SelectionSource_SelectionChanged(object sender, RoutedEventArgs e)
{
var targetTextBox = ((TextBox) sender).Tag as TextBox;
if (targetTextBox != null)
{
var bndExp
= BindingOperations.GetBindingExpression(
targetTextBox, TextBox.TextProperty);
if (bndExp != null)
{
bndExp.UpdateTarget();
}
}
}
If you are using MVVM then handle this SelectionSource_SelectionChanged event in attached behavior.
EDIT 2:
Now in case if one text box is part of ListBox template and other is outside the template then use content control hack...
XAML:
<Window.Resources>
<TextBox x:Key="SelectionTarget"
Text="{Binding Tag.SelectedText,
RelativeSource={RelativeSource Self},
Mode=TwoWay,
UpdateSourceTrigger=Explicit}" />
</Window.Resources>
<StackPanel>
<ListBox>
<ListBox.ItemsSource>
<x:Array Type="{x:Type System:String}">
<System:String>Test String 1</System:String>
<System:String>Test String 2</System:String>
<System:String>Test String 3</System:String>
</x:Array>
</ListBox.ItemsSource>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Name="SelectionSource"
Text="{Binding Path=., Mode=TwoWay}"
Tag="{StaticResource SelectionTarget}"
SelectionChanged="SelectionSource_SelectionChanged" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ContentControl Content="{StaticResource SelectionTarget}">
</ContentControl>
</StackPanel>
Code Behind
private void SelectionSource_SelectionChanged(
object sender, RoutedEventArgs e)
{
var targetTextBox
= ((TextBox) sender).Tag as TextBox;
if (targetTextBox != null)
{
targetTextBox.Tag = (TextBox) sender;
var bndExp
= BindingOperations.GetBindingExpression(
targetTextBox, TextBox.TextProperty);
if (bndExp != null)
{
bndExp.UpdateTarget();
}
}
}
Hope this helps.
I'm not really sure what's going on with "SelectedText" you are trying to bind to, but if all you are trying to do is display the "lvItems" SelectedItem text in your "headText" TextBox the following should work
<TextBox Name="headText" Text="{Binding ElementName=lvItems, Path=SelectedItem.headerObj.headervalue}" />
You'll need to change your TextBox "A" binding as well.
<TextBox Name ="A" Tag="Header" Text="{Binding headerObj.headervalue}" IsReadOnly="True" BorderThickness="0" >
</TextBox>
Assuming that headerObj is a property of the Packet class, and headervalue is a property of that, and headervalue is the value you wish to bind to.
The text in "headText" will update when the SelectedItem is changed (not when the TextBox is clicked).