WPF MVVM Binding to a DataTable within a DataTable - wpf

I have created a DataTable with an unknown number of columns.
There is only one Row within the DataTable and it contains another DataTable for each cell.
CoordsDataTable = new();
foreach (var coordset in DrillDesigns.Coordinates)
{
CoordsDataTable.Columns.Add(coordset!.CoordinateType!.CoordinateType, typeof(DataTable));
}
CoordsDataTable.Rows.Add();
foreach (var coordset in DrillDesigns.Coordinates)
{
CoordsDataSet = new();
CoordsDataSet.Columns.Add("X");
CoordsDataSet.Columns.Add("Y");
CoordsDataSet.Columns.Add("Z");
CoordsDataSet.Rows.Add(coordset.X, coordset.Y, coordset.Z);
CoordsDataTable.Rows[0][coordset!.CoordinateType!.CoordinateType] = CoordsDataSet;
}
How do I bind to this in WPF MVVM?
I can get the first table with autogenerated columns but I cannot bind to the second DataTable.
<DataGrid
x:Name="coordsDG"
ItemsSource="{Binding CoordsDataTable }"
DockPanel.Dock="Top">
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<DataGrid
ItemsSource="{Binding DataContext, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"
AutoGenerateColumns="True"
CanUserAddRows="False">
</DataGrid>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid>

Related

DataGridColumn binding fail

This is my DataGrid;
<DataGrid Visibility="Visible"
Grid.Row="1"
SelectionUnit="CellOrRowHeader"
Name="dataGrid"
SelectionMode="Single"
ItemsSource="{Binding collcection}">
<DataGrid.ContextMenu>
<ContextMenu>
<MenuItem Command="Copy" Click="MenuItem_Click_1"/>
</ContextMenu>
</DataGrid.ContextMenu>
<DataGrid.Columns>
<DataGridTemplateColumn Header="Select">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox
x:Name="cbRunRobot"
IsChecked="{Binding Value}"
Width="60"
Height="25"
Checked="cbRunRobot_Checked"
Unchecked="cbRunRobot_Unchecked"
Margin="25,0,0,0" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
View Model:
list<MyData> collcection;
public class MyData
{
public string Name;
public string Id;
}
I try to add this Column:
<DataGridColumn Binding="{Binding Name}" Header="Name" Width="180"/>
And got this errors:
Error 1 The type "DataGridColumn" is abstract and must include an explicit value.
Error 2 The member "Binding" is not recognized or is not accessible.
DataGridColumn is an abstract class which means it cannot be instantiated. The same applies to DataGridBoundColumn.
You're choices are:
DataGridCheckBoxColumn for boolean values
DataGridComboBoxColumn for enumerable values
DataGridHyperlinkColumn for Uri values
DataGridTemplateColumn to show any types of data by defining your own cell template
DataGridTextColumn to show text values
It looks like DataGridTextColumn is what you're looking for.
Hi I can suggest you the next:
Bind to ObservableCollection instead the list.
Make your MyData model to implement InotifyPropertyChanged.
Make each binding involved property in MyData model to fire OnPropertyChanged event.
Here is the link to the working example:How to Display and select items in a Datagrid ComboBox with WPF C#, using MVVM.
regards,

Programmatically enter edit mode on datagrid row

i have following request: items are added and removed from datagrid (WPF) with button click. When user adds new row (new item) in datagrid, I want to automatically enter edit mode on this new item. This would work if SelectionUnit would be Cell, but I would like it to be FullRow. I tried to change SelectionUnit to Cell before calling datagrig.BeginEdit() but it doesn't work.
Here's the code:
<DataGrid Name="dgDatagrid" Margin="20,20,3,3" CanUserSortColumns="True" ItemsSource="{Binding AllItems}"
AutoGenerateColumns="False" SelectionUnit="FullRow" SelectionMode="Single" CanUserAddRows="False" CanUserDeleteRows="False"
CanUserResizeRows="True" CanUserReorderColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="First column name" MinWidth="160" Binding="{Binding Path=FirstColumn}"/>
<DataGridCheckBoxColumn Header="Second column name" MinWidth="100" Binding="{Binding Path=SecondColumn, UpdateSourceTrigger=PropertyChanged}"/>
</DataGrid.Columns>
</DataGrid>
And background functionality:
private void AddNewItem()
{
var newItem = new Item();
AllItems.Add(newItem);
dgDatagrid.ScrollIntoView(newItem );
dgDatagrid.SelectedItem = newItem ;
var selectedIndex = dgDatagrid.SelectedIndex;
dgDatagrid.SelectionUnit = DataGridSelectionUnit.Cell;
dgDatagrid.CurrentCell = new DataGridCellInfo(dgDatagrid.Items[selectedIndex], dgDatagrid.Columns[0]);
dgDatagrid.BeginEdit();
dgDatagrid.SelectionUnit = DataGridSelectionUnit.FullRow;
}

GridView is not adding rows to collection

I`m developing WPF application based on MVVM.
I need to create DataGrid with 2 ComboBox columns.
I created the next grid:
<DataGrid Grid.Column="1" Grid.Row="4" AutoGenerateColumns="False" Margin="0,8,20,8" CanUserAddRows="True" CanUserDeleteRows="True" ItemsSource="{Binding MapsGrid}">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Main Category" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox
ItemsSource="{Binding DataContext.MainCategories, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
DisplayMemberPath="Category"
SelectedItem="{Binding DataContext.MainCategorySelectedItem, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Sub Category" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox
ItemsSource="{Binding DataContext.SubCategories, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
DisplayMemberPath="Category"
SelectedItem="{Binding DataContext.SubCategorySelectedItem, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
The grid looks exactly like I need and the ComboBox controls contains data but I don`t know why the grid not insert new rows to my collection.
In my view model I have the next collection:
private ObservableCollection<MapsDescGridModel> _mapsGrid;
public ObservableCollection<MapsDescGridModel> MapsGrid
{
get { return _mapsGrid; }
set
{
if (Equals(value, _mapsGrid)) return;
_mapsGrid = value;
RaisePropertyChanged("MapsGrid");
}
}
I initiliaze it in my constructor and I'm seeing a blank row in my datagrid but I can't add rows (I`m trying with Enter key)
The object "MapsDescGridModel" contains 2 entities (Entity framework entities)
public class MapsDescGridModel: NotificationObject
{
public MapsDescGridModel()
{
}
public MapsDescGridModel(MainCategories mainCat, SubCategories subcat)
{
MainCategory = mainCat;
SubCatergory = subcat;
}
private MainCategories _mainCategory;
public MainCategories MainCategory
{
get { return _mainCategory; }
set
{
if (Equals(value, _mainCategory)) return;
_mainCategory = value;
RaisePropertyChanged("MainCategory");
}
}
private SubCategories _subCatergory;
public SubCategories SubCatergory
{
get { return _subCatergory; }
set
{
if (Equals(value, _subCatergory)) return;
_subCatergory = value;
RaisePropertyChanged("SubCatergory");
}
}
}
}
I tried to add columns by code but I can see only one row (all the rest are copy of this row).
What can be the problem?
DataGridColumns wont be added to the visual tree. Thats why bindings which use the vt (e.g. ElementName, FindAncestor bindings) don't work there.
This problem look similar to this one.
Btw.: How does it make sense to bind the SelectedItem of each row to your main DataContext if there are many rows but only one DataContext? You should bind your SelectedItem of each row to a property in your MapsDescGridModel class.

How to figure out if a DataGrid Row Has been Changed in MVVM?

I want to Bind the WpfDatagrid Rows to a boolean Property in a ViewModel,that shows if Row Has been changed.in the fact I have a datagrid that bind to a class in the model, and have a property IsRowChanged in the ViewModel,and don't know how to bind datagrid to IsRowChanged ?
<DataGrid ItemsSource="{Binding Produts}" AutoGenerateColumns="False" >
<DataGrid.Columns>
<DataGridTemplateColumn IsReadOnly="True" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsRowChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
If you use entity framework you can directly extend your entity partial class with this line
public bool IsRowChanged { get { return myEntity.EntityState == EntityState.Modified; } }

How to implement editable DataGridComboBoxColumn in WPF DataGrid

I want to enable the user to edit some data in WPF DataGrid ( from the .net Framework 4.0). The "instruments" column should allow the user to select an available intrument from a static list or to write a free text.
My DataGrid is binded to data using MVVM. I've tried many solutions I've found in internet but none of them work correctly.
Here is my code:
<DataGrid Margin="0,6" ItemsSource="{Binding Path=Orders}" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False" CanUserResizeRows="True">
<DataGrid.Columns>
<DataGridComboBoxColumn Header="Instrument" MinWidth="140"
ItemsSource="{x:Static ViewModel.Instruments}" SelectedItemBinding="{Binding Path=SelectedInstrument}">
<DataGridComboBoxColumn.EditingElementStyle>
<Style TargetType="ComboBox">
<Setter Property="IsEditable" Value="True"/>
</Style>
</DataGridComboBoxColumn.EditingElementStyle>
</DataGridComboBoxColumn>
</DataGrid.Columns>
</DataGrid>
The drop-down-list is shown correctly. The field can be edited with any text, but it sets a null to the SelectedInstrument after the drop-down is closed for the free text. It works only for the selected item. I've tried to change to SelectedValueBinding, but it doesn't help.
How to implement this requirements properly? Can someone post here a working sample?
Additional:
Orders is ObservableCollection
Order has Property like string Title, DateTime Ordered, string SelectedInstrument,
Instruments is a string[]
Solutions:
Following suggest as a workaround from bathineni works:
<DataGrid Margin="0,6" ItemsSource="{Binding Path=Orders}" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False" CanUserResizeRows="True">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Instrument" MinWidth="140">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=SelectedInstrument, Mode=OneWay}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox IsEditable="True" Text="{Binding Path=SelectedInstrument}"
ItemsSource="{x:Static ViewModel.Instruments}"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
this is happening because the free text which is enter is of type string and selected item what you have binded to the comboBox is of some complex type....
instead of using DataGridComboBoxColumn use DataGridTemplateColumn and you can bind Text property of the comboBox to some property which will hold the free text value after closing drop down list.
you can get better idea by looking at the following sample.
<DataGrid>
<DataGrid.Columns>
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox IsEditable="True"
Text="{Binding NewItem}"
ItemsSource="{Binding Sourcelist.Files}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
Try to use SelectedValue only but along with it use DisplayMemberPath and TextSearch.TextPath.
<ComboBox IsEditable="True" DisplayMemberPath="MyDisplayProperty" SelectedValuePath="MyValueProperty" SelectedValue="{Binding MyViewModelValueProperty}" TextSearch.TextPath="MyDisplayProperty" />
For editable comboboxes we must synchronize what value the combo selects, what value the items display and what value we must search based on user input.
But If you are using a string collection to bind your combobox then you can try following...
Add a new property in your ViewModel called InstrumentsView. This returns a new ListCollectionView.
public static string ListCollectionView InstrumentsView
{
get
{
return new ListCollectionView(Instruments);
}
}
Change your DataGridComboBoxColumn XAML as below...
<DataGridComboBoxColumn Header="Instrument" MinWidth="140"
ItemsSource="{x:Static ViewModel.InstrumentsView}">
<DataGridComboBoxColumn.EditingElementStyle>
<Style TargetType="ComboBox">
<Setter Property="IsEditable" Value="True"/>
<Setter Property="IsSynchronizedWithCurrentItem" Value=True" />
<Setter Property="SelectedItem" Value="{Binding SelectedInstrument, Mode=OneWayToSource}" /> <!-- Assuming that SelectedInstrument is string -->
</Style>
</DataGridComboBoxColumn.EditingElementStyle>
</DataGridComboBoxColumn>
Tell me if this works....
You can create your own ComboBox column type by subclassing DataGridBoundColumn. Compared to bathineni's solution of subclassing DataGridTemplateColumn the below solution has the benefit of better user experience (no double-tabbing) and you have more options to tune the column to your specific needs.
public class DataGridComboBoxColumn : DataGridBoundColumn {
public Binding ItemsSourceBinding { get; set; }
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem) {
var textBox = new TextBlock();
BindingOperations.SetBinding(textBox, TextBlock.TextProperty, Binding);
return textBox;
}
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem) {
var comboBox = new ComboBox { IsEditable = true };
BindingOperations.SetBinding(comboBox, ComboBox.TextProperty, Binding);
BindingOperations.SetBinding(comboBox, ComboBox.ItemsSourceProperty, ItemsSourceBinding);
return comboBox;
}
protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs) {
var comboBox = editingElement as ComboBox;
if (comboBox == null) return null;
comboBox.Focus(); // This solves the double-tabbing problem that Nick mentioned.
return comboBox.Text;
}
}
You can then use the component for example like this.
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding MyItems}">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
<local:DataGridComboBoxColumn Header="Thingy" Binding="{Binding Thingy}"
ItemsSourceBinding="{Binding
RelativeSource={RelativeSource AncestorType={x:Type local:MainWindow}},
Path=Thingies}"/>
</DataGrid.Columns>
</DataGrid>
I got this solution by following this answer to a similar question.
Maybe it'll still be useful to someone. This solution allows to add new entered values to selection list and has no side effects while editing.
XAML:
<DataGridTemplateColumn Header="MyHeader" Width="Auto">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox IsEditable="True"
Text="{Binding MyTextProperty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
DisplayMemberPath="MyTextProperty"
SelectedValuePath="MyTextProperty"
ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}, Path=DataContext.SelectionList}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
ViewModel:
public class MyViewModel
{
public class MyItem : INotifyPropertyChanged {
private string myTextProperty;
public string MyTextProperty {
get { return myTextProperty; }
set { myTextProperty = value;
OnPropertyChanged("MyTextProperty"); }
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName]string prop = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
}
public ObservableCollection<MyItem> MyItems { get; set; }
public object SelectionList { get; set; }
}
CodeBehinde:
MyWindow.DataContext = MyViewModelInstance;
MyDataGrid.ItemsSource = MyItems;
// Before DataGrid loading and each time after new MyProperty value adding, you must execute:
MyViewModelInstance.SelectionList = MyViewModelInstance.MyItems.OrderBy(p => p.MyTextProperty).GroupBy(p => p.MyTextProperty).ToList();

Resources