Declaratively adding row and controls to grid in WPF - c#

Well, my last question did not garner any response except a downvote, so I am going to break down the question and ask one elementary question at a time.
I have a WPF Application, in which there is a Grid (not DataGrid). Now I want to add rows to it at runtime. And in each of those rows will be a control like TextBox or Combobox. I understand this can be done by RowDefinitions.Add(New RowDefinition()), and then adding individual controls to each cell in the row, like we did in TableLayoutPanel in WinForms. But I was looking for a more elegant solution, where all controls with their event handlers would be added to the respective cells in a new row of the Grid when a button is clicked or an event is triggered. Is there any easy way to do it?
P.S. I also need to delete the rows as required, if that is a factor here. And the row deleted is not always the last row.

Normally, it is better to use DataGrid to display data with repeating rows, not
a Grid. But if you have special reasons to use a Grid, I have attached a code sample using a Grid, displaying in each row a Textbox, a ComboBox and a DeleteButton. An Add button allows to add new rows at the bottom and the DeleteButtons on every row allow the deletion of that row.
XAML:
<Window x:Class="GridAddRow.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:GridAddRow"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<DockPanel>
<Grid Name="MainGrid" DockPanel.Dock="Top"/>
<Button DockPanel.Dock="Top" Name="AddButton" Content="Add"/>
<Rectangle Fill="Gainsboro"/>
</DockPanel>
</Window>
C#:
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
namespace GridAddRow {
public partial class MainWindow: Window {
int textCol, comboBoxCol, buttonCol;
public MainWindow() {
InitializeComponent();
//initialise MainGrid, i.e. define columns, could be done in XAML
addCol(out textCol, new GridLength(100));
addCol(out comboBoxCol, GridLength.Auto);
addCol(out buttonCol, GridLength.Auto);
//initialise grid content, usually read from a db
addRow("Some Text", 1);
addRow("Another String", 3);
AddButton.Click += AddButton_Click;
}
private void addCol(out int textCol, GridLength gridLength) {
textCol = MainGrid.ColumnDefinitions.Count;
MainGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = gridLength });
}
List<TextBox> textBoxes = new List<TextBox>();
List<ComboBox> comboBoxes = new List<ComboBox>();
private void addRow(string textBoxString, int comboBoxIndex) {
int rowId = MainGrid.RowDefinitions.Count;
MainGrid.RowDefinitions.Add(new RowDefinition());
TextBox textBox = new TextBox { Text = textBoxString };
textBoxes.Add(textBox);
addControl(textBox, rowId, textCol);
textBox.TextChanged += TextBox_TextChanged;
ComboBox comboBox = new ComboBox {SelectedIndex= comboBoxIndex };
comboBox.Items.Add(new ComboBoxItem {Content="0" });
comboBox.Items.Add(new ComboBoxItem { Content="1" });
comboBox.Items.Add(new ComboBoxItem { Content="2" });
comboBox.Items.Add(new ComboBoxItem { Content="3" });
comboBoxes.Add(comboBox);
addControl(comboBox, rowId, comboBoxCol);
comboBox.SelectionChanged += ComboBox_SelectionChanged;
Button deleteRowButton = new Button {Content = "Delete"};
addControl(deleteRowButton, rowId, buttonCol);
deleteRowButton.Click += DeleteRowButton_Click;
}
private void addControl(Control control, int rowId, int textCol) {
control.Tag = rowId;
MainGrid.Children.Add(control);
Grid.SetRow(control, rowId);
Grid.SetColumn(control, textCol);
}
private void TextBox_TextChanged(object sender, TextChangedEventArgs e) {
TextBox textbox = (TextBox)sender;
int rowid = (int)textbox.Tag;
//do something here.
}
private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) {
ComboBox comboBox = (ComboBox)sender;
int rowid = (int)comboBox.Tag;
//do something here.
}
private void DeleteRowButton_Click(object sender, RoutedEventArgs e) {
Button deleteButton = (Button)sender;
int rowid = (int)deleteButton.Tag;
MainGrid.Children.Remove(deleteButton);
TextBox textbox = textBoxes[rowid];
MainGrid.Children.Remove(textbox);
textBoxes.RemoveAt(rowid);
ComboBox comboBox = comboBoxes[rowid];
MainGrid.Children.Remove(comboBox);
comboBoxes.RemoveAt(rowid);
}
private void AddButton_Click(object sender, RoutedEventArgs e) {
addRow("", 1);
}
}
}
Saying "you should use MVVM" basically just means that you should create a class which has one property for every control (Textbox, Combobox) you use in your Window. Create a collection of that class with the data to be displayed, then bind it to the DataGrid, which will display the data and offers lots of functionality, which you have to program yourself when using a Grid. For more details see my article: Guide to WPF DataGrid formatting using bindings

Related

How to 'wrap' the columns (not the text/content) of a WPF DataGrid?

I'm trying to implement a solution where I can 'wrap' a WPF DataGrid - by that I mean that the entire columns and rows wrap, not their text or content (see image below).
I have a set of data comprised of columns and rows (with column headers) that I want to wrap when constricting the window's width constraints, rather than instead using a horizontal scroll bar which would mean data is presented off-screen.
I had a look at using a WrapPanel as the ItemsPanelTemplate of my DataGrid, however I was not able to build on this further to achieve what I wanted.
<DataGrid.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</DataGrid.ItemsPanel>
If there is an answer that achieves what I want using another control, i.e. a ListView or GridView without compromises, I would be happy with that.
My current solution is to manually modify my ItemsSource and break that up, and then create multiple DataGrids of a pre-determined size, which is not very flexible.
Testing DataGrid.ItemsPanel and WrapPanel
The DataGrid only displays rows of data. So, if we set WrapPanel.Orientation to "Horizontal":
<DataGrid x:Name="dg" ItemsSource="{Binding _humans}">
<DataGrid.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</DataGrid.ItemsPanel>
</DataGrid>
Each row will be positioned side by side horizontally:
ListView and GridView
If we want to display Property columns separately, we should use a ListView for each property. The GridView will be used for displaying the header. In the XAML document we set a Name for the WrapPanel.
<Grid>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<WrapPanel Name="wraper" Orientation="Horizontal">
</WrapPanel>
</ScrollViewer>
</Grid>
The code behind is implemented as:
public partial class MainWindow : Window
{
private IList<Human> _humans;
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
CreateHumans();
wraper.Children.Add(CreateListViewFor("FirstName"));
wraper.Children.Add(CreateListViewFor("LastName"));
wraper.Children.Add(CreateListViewFor("Age"));
wraper.Children.Add(CreateListViewFor("Bday", "Birthday"));
wraper.Children.Add(CreateListViewFor("Salary"));
wraper.Children.Add(CreateListViewFor("Id", "Identification"));
}
private void CreateHumans()
{
_humans = new List<Human>();
for (int i = 10; i < 20; i++)
{
var human = new Human();
human.FirstName = "Zacharias";
human.LastName = "Barnham";
human.Bday = DateTime.Parse("1.3.1990");
human.Id = "ID-1234-zxc";
human.Salary = 2_000_000;
_humans.Add(human);
}
}
private ListView CreateListViewFor(string propertyName, string header)
{
var lv = new ListView();
var gv = new GridView();
lv.ItemsSource = _humans;
lv.View = gv;
lv.SelectionChanged += UpdateSelectionForAllListViews;
gv.Columns.Add(new GridViewColumn() { Header = header, DisplayMemberBinding = new Binding(propertyName), Width = 100 });
return lv;
}
private ListView CreateListViewFor(string propertyName)
{
return CreateListViewFor(propertyName, propertyName);
}
private void UpdateSelectionForAllListViews(object sender, SelectionChangedEventArgs e)
{
int index = (sender as ListView).SelectedIndex;
foreach (var child in wraper.Children)
{
(child as ListView).SelectedIndex = index;
}
}
}
We call our CreateListViewFor() method to provide ListView objects to the WrapPanel. We create a callback for the Selection Changed event to update the selection for each list. The styling is up to you.
The next gif shows the final result:

Can you retrieve the datagrid object with just a DataGridRow? WPF

I'm trying to retrieve the datagrid because I want to focus on a specific cell in a row. I have a DataGridRow based on the LoadingRow event that I use by doing this:
<i:EventTrigger EventName="LoadingRow">
<utils:InteractiveCommand Command="{Binding RelativeSource = {RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.MainWindowViewModel.SDataGrid_LoadingRow}"/>
</i:EventTrigger>
But in the function receiving this, I only am able to get the DataGridRow.
public void SDataGridLoadingRow(object param)
{
DataGridRowEventArgs e = param as DataGridRowEventArgs;
e.Row.Tag = e.Row.GetIndex().ToString();
}
I want to get a specific cell from the row and focus on it so the user can type. Is this possible?
I'm using MVVM
Also have this now
public void SDataGridLoadingRow(object sender, DataGridRowEventArgs e)
{
e.Row.Tag = e.Row.GetIndex().ToString();
DataGrid dataGrid = sender as DataGrid;
dataGrid.Focus();
// Cancel our focus from the current cell of the datagrid
// if there is a current cell
if (dataGrid.CurrentCell != null)
{
var cancelEdit = new System.Action(() =>
{
dataGrid.CancelEdit();
});
Application.Current.Dispatcher.BeginInvoke(cancelEdit,
System.Windows.Threading.DispatcherPriority.ApplicationIdle, null);
}
dataGrid.CurrentCell = new DataGridCellInfo(
dataGrid.Items[e.Row.GetIndex()], dataGrid.Columns[1]);
var startEdit = new System.Action(() =>
{
dataGrid.BeginEdit();
});
Application.Current.Dispatcher.BeginInvoke(startEdit,
System.Windows.Threading.DispatcherPriority.ApplicationIdle, null);
}
And the previous row is still in edit mode... can't quite figure out how to get it out of edit mode...
I am not sure why your LoadingRow event handler is in your ViewModel.
If you are using MVVM your viewModels shouldn't be manipulating visual element such as DataGrid and DataGridCell but only the underlying business data.
In your case you could subscribe to the LoadingRow event like this:
<DataGrid ItemsSource="{Binding BusinessObjectExemples}" LoadingRow="DataGrid_LoadingRow" />
and then in your code behind (xaml.cs file):
private void DataGrid_LoadingRow(object sender, DataGridRowEventArgs e)
{
if (sender is DataGrid dataGrid && e.Row is DataGridRow row)
{
//You can now access your dataGrid and the row
row.Tag = row.GetIndex().ToString();
//The grid is still loading row so it is too early to set the current cell.
}
}
What you can do is subscribe to the loaded event of your grid and set the selectedCell there:
private void Grid_Loaded(object sender, RoutedEventArgs e)
{
//Adapt the logic for the cell you want to select
var dataGridCellInfo = new DataGridCellInfo(this.Grid.Items[11], this.Grid.Columns[1]);
//The grid must be focused in order to be directly editable once a cell is selected
this.Grid.Focus();
//Setting the SelectedCell might be neccessary to show the "Selected" visual
this.Grid.SelectedCells.Clear();
this.Grid.SelectedCells.Add(dataGridCellInfo);
this.Grid.CurrentCell = dataGridCellInfo;
}
You could also execute the same logic with a button.
Xaml:
<DataGrid x:Name="Grid" ItemsSource="{Binding BusinessObjectExemples}"
Loaded="Grid_Loaded" SelectionUnit="Cell" AutoGenerateColumns="False"
LoadingRow="DataGrid_LoadingRow">
And if some of the treatment is related to the business and needs to be in your viewModel. You can then invoke a command or run public methods from DataGrid_LoadingRow in your code behind.

SourceUpdated with NotifyOnSourceUpdated not fired in ListBox

I'm trying to use a SourceUpdated event in ListBox, but it doesn't fire.
I have binded ObservableCollection to ItemSource of ListBox, set NotifyOnSourceUpdated = true, and binding is working correctly - after adding new item to the collection, the ListBox displays new item, but without fireing the event.
MainWindow.xaml.cs:
public partial class MainWindow:Window
{
public ObservableCollection<string> Collection { get; set; }
public MainWindow()
{
InitializeComponent();
Collection = new ObservableCollection<string>();
var custBinding = new Binding()
{
Source = Collection,
NotifyOnSourceUpdated = true
};
intList.SourceUpdated += intList_SourceUpdated;
intList.SetBinding(ItemsControl.ItemsSourceProperty, custBinding);
}
private void intList_SourceUpdated(object sender, DataTransferEventArgs e)
{
MessageBox.Show("Updated!");
}
private void btnAddInt_Click(object sender, RoutedEventArgs e)
{
var randInt = new Random().Next();
Collection.Add(randInt.ToString());
}
}
MainWindow.xaml:
<Window x:Class="Test.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:Test"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Button x:Name="btnAddInt" Content="Button" HorizontalAlignment="Left" Margin="41,31,0,0" VerticalAlignment="Top" Width="75" Click="btnAddInt_Click"/>
<ListBox x:Name="intList" HorizontalAlignment="Left" Height="313" Margin="160,31,0,0" VerticalAlignment="Top" Width="600"/>
</Grid>
What am I missing, that it's not working ?
Thank you for an advice.
SourceUpdated is only fired on elements that can accept input and change the databound source value directly.
https://msdn.microsoft.com/en-us/library/system.windows.data.binding.sourceupdated(v=vs.110).aspx
In this case, the listbox itself is not updating the collection, rather the collection is being updated via a button click. This has nothing to do with the listbox firing the SourceUpdated Event.
Only input elements that can accept input like a textboxes, checkboxes, radio buttons and custom controls that use those controls will be able to two-way bind and transfer their values back to the source it is bound to.
You may be looking for CollectionChanged which will fire when items are added or removed from the Collection.
https://msdn.microsoft.com/en-us/library/ms653375(v=vs.110).aspx
Collection = new ObservableCollection<string>();
Collection.CollectionChanged += (s, e) =>
{
// collection changed!
};
Hope this helps! Cheers!

WPF TextBox.Text doesn't update in another control

It is simple thing that works everywhere instead of TextBox.Text without any reasons. What I need is just to get updated Text property of textbox when DataContext changed. There is a DataGrid and another control behind it to show details about row. So when user click's on row I am obtain data object from grid's row and show its details in this details control. Details control contains just propertyName and value control. I.e. textblock and textbox. ANY property of textblock or textbox are updated on changing DataContext except Text in textbox. I've break my head already.
The main problem is textbox won't updated after I've changed it. And it works fine if move textbox outside control to the details control (parent)
The property-Value control:
<DockPanel x:Class="UserControls.PropertySectionControl"
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"
mc:Ignorable="d"
x:Name="_propertyDockPanel"
HorizontalAlignment="Stretch" Margin="0 5 0 0"
d:DesignHeight="300" d:DesignWidth="300">
<TextBlock Text="{Binding Path=Property, ElementName=_propertyDockPanel}" TextWrapping="Wrap"/>
<TextBox
Text="{Binding Path=Value, ElementName=_propertyDockPanel, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
IsReadOnly="{Binding Path=IsReadOnly, ElementName=_propertyDockPanel}"
TextAlignment="Right" Foreground="Gray" TextWrapping="Wrap" Width="120" Height="20" HorizontalAlignment="Right" Margin="0 0 15 0"
ToolTip="{Binding Value, ElementName=_propertyDockPanel}" Template="{StaticResource _inspectorValueTemplate}"
LostFocus="_textBoxValue_LostFocus" GotFocus="_textBoxValue_LostFocus" KeyDown="_textBoxValue_KeyDown" TextChanged="_textBoxValue_TextChanged"
/>
</DockPanel>
And here is how it is used in details control:
<userControls:PropertySectionControl Property="Total" Value="{Binding OrderCharges.Total}" IsReadOnly="True"/>
As you can see, in PropertySectionControl there is also binding ToolTip to Value which is works on DataContext changed! But for TextBox.Text is not. What is this?
UPD:
PropertySectionControl.cs
public partial class PropertySectionControl : DockPanel
{
public string Property
{
get { return (string)GetValue(PropertyProperty); }
set { SetValue(PropertyProperty, value); }
}
public string Value
{
get { return (string)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public bool IsReadOnly
{
get { return (bool)GetValue(IsReadOnlyProperty); }
set { SetValue(IsReadOnlyProperty, value); }
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(string), typeof(PropertySectionControl), new PropertyMetadata(""));
public static readonly DependencyProperty PropertyProperty =
DependencyProperty.Register("Property", typeof(string), typeof(PropertySectionControl), new PropertyMetadata(""));
public static readonly DependencyProperty IsReadOnlyProperty =
DependencyProperty.Register("IsReadOnly", typeof(bool), typeof(PropertySectionControl), new PropertyMetadata(false));
public PropertySectionControl()
{
InitializeComponent();
}
/// <summary>
/// Static event handler is for use in Inspector control as well
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public static void TextBox_LostOrGotFocus(object sender, RoutedEventArgs e)
{
var textBox = sender as TextBox;
if (textBox.IsFocused)
{
textBox.TextAlignment = TextAlignment.Left;
textBox.SelectAll();
}
else
{
textBox.TextAlignment = TextAlignment.Right;
}
}
public static void TextBox_KeyDown(object sender, KeyEventArgs e)
{
var textBox = sender as TextBox;
if (e.Key == Key.Enter)
{
textBox.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
}
else if (e.Key == Key.Escape)
{
int i = 0;
do
{
textBox.MoveFocus(new TraversalRequest(FocusNavigationDirection.Right));
} while (!(Keyboard.FocusedElement is TextBoxBase) && ++i < 5); // prevent infinite loop
Keyboard.ClearFocus();
}
}
public static void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
var textBox = sender as TextBox;
textBox.Height = 20;
for (int i = 2; i <= textBox.LineCount; i++)
textBox.Height += 14;
}
private void _textBoxValue_LostFocus(object sender, RoutedEventArgs e)
{
TextBox_LostOrGotFocus(sender, e);
}
private void _textBoxValue_KeyDown(object sender, KeyEventArgs e)
{
TextBox_KeyDown(sender, e);
}
private void _textBoxValue_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox_TextChanged(sender, e);
}
}
Just guessing:
1) That textbox has a textchanged eventhandler set. Check if that handler works OK. Try to comment it out ans see if it changes anything. I.e. maybe it gets fired during the data update from model and maybe it throws an exception and binding is aborted in the meantime?
1a) run in VS in Debug mode and check 'Output' window. See if there is anything reported like:
first chance exceptions
binding errors
important especially if they show up when you try editing the textbox for the first time
2) Also, whenever a binding magically stops working, be sure to check if the binding is still in place. I see you are using a direct access to the controls from code-behind (like textbox.Height += ..). This is an easy way to break the bindings. If you ever anywhere ran one of these lines:
textBox.Text = ""
textBox.Text = "foo"
textBox.Text = john.name
textBox.Text += "."
these may have a high chance of unsetting your bindings on Text on that textbox. I don't see any such line in the code you provided, but maybe you have it elsewhere.
You can easily check if the binding is still inact by running:
object realvalue = textBox.ReadLocalValue(TextBox.TextProperty);
now if the realvalue is null, or string, or anything other than a Binding object - that means that something accessed the textbox and replaced the binding with a concrete constant value, and you need to find and correct that so that .Text is not assigned and instead of that the Value property of source object (customdockpanel) is changed.

WPF Fire event for dynamically added control in stack panel

I am adding adding a radio button to stack panel for each item in List and that happens without issue. (it adds 4 or 6 radio buttons as those would be count of items in my list).
foreach (var nearestgage in nearestgages)
{
StackPanel.Children.Add(new RadioButton { Content = nearestgage.GageSize.ToString(), Margin = new Thickness(1, 1, 1, 1) });
}
Now what i want to do is when any of these dynamically created radio buttons are selected at run time.
I want to fire a event.
So what i am thinking is i will have to attach a handler to my radio button click . I tried a few methods but not able to that. I have another radio button in the grid which does not belong to this group as well. Please suggest what would be the ideal way to do this.
Here's a little sample, adding the event to the radio button when it's generated.
XAML:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"
Loaded="Window_Loaded">
<StackPanel Name="StackPanel">
</StackPanel>
</Window>
C#
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
for (int i = 1; i <= 10; i++)
{
RadioButton rb = new RadioButton();
rb.Content = "Item " + i.ToString();
rb.Click += rb_Click;
StackPanel.Children.Add(rb);
}
}
void rb_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show((sender as RadioButton).Content.ToString());
}
}
To deal with keeping the radio button actions separate, set the group name:
rb.GroupName = "Dynamic";

Categories