WPF MVVM Databinding Nested Datagrid - wpf

I'm a beginner to WPF and MVVM pattern. I still have a issue with databinding in case of backward binding. I would like to bind selectedValues of the Comboboxes into my Oberservable Collection. I learned is possible to bind the value of a combobox to a property, but in this case I would like to bind to a property of a collection and the collection is the parent.
Could somebody explain me how to bind the combobox selectedvalues to my observablecollection?
I have a workaround in my mind to make for each combobox a new property in my viewmodel and collect all values and store by button press this values into my collection, but this seems for me wrong, because is not the behavouir of databinding.
EDIT The comboboxes items are corrected populated from each model, but how i can bind the selectedvalue of the combobox to my collection property?
<ComboBox ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type Window}},
Path=DataContext.Cities}"
DisplayMemberPath="Name"
SelectedValue="{Binding Path=RowEntries }"/>
The SelectedValue="{Binding Path=RowEntries}" is wrong or is this correct?
EDIT 2
I added a Listview binded to my collection to see, if the properties are binded to the selectedvalue of my combobox, but is keeps empty.
<ListView ItemsSource="{Binding RowEntries}" BorderBrush="Black" BorderThickness="1">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=CityName}"></TextBlock>
<TextBlock Text="{Binding Path=CountryName}"></TextBlock>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I have this solution:
My Models:
// INPC implemented by Propertychanged.Fody
public class RowEntry : BaseModel
{
public string CityName { get; set; }
public string CountryName { get; set; }
}
// INPC implemented by Propertychanged.Fody
public class City : BaseModel
{
public string Name { get; set; }
}
// INPC implemented by Propertychanged.Fody
public class Country : BaseModel
{
public string Name { get; set; }
}
My ViewModel:
public class TestViewModel : ViewModelBase
{
#region properties
// INPC implemented by Propertychanged.Fody
public ObservableCollection<City> Cities { get; set; } = new ObservableCollection<City>();
public ObservableCollection<Country> Countries { get; set; } = new ObservableCollection<Country>();
public ObservableCollection<RowEntry> RowEntries { get; set; } = new ObservableCollection<RowEntry>();
#endregion
#region constructors and destructors
/// <summary>
/// Initializes a new instance of the MainViewModel class.
/// </summary>
public TestViewModel()
{
// Sample Data
var temp = new City { Name = "Rom" };
Cities.Add(temp);
var temp2 = new City { Name = "Sydney" };
Cities.Add(temp2);
var temp3 = new Country { Name = "Italy" };
Countries.Add(temp3);
var temp4 = new Country { Name = "Australia" };
Countries.Add(temp4);
RowEntries.Add(new RowEntry());
}
#endregion
}
My Ui:
<StackPanel>
<DataGrid ItemsSource="{Binding RowEntries}" AlternationCount="{Binding Items.Count, RelativeSource={RelativeSource Self}}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding AlternationIndex, RelativeSource={RelativeSource AncestorType=DataGridRow}}" Header="#"/>
<DataGridTemplateColumn Header="City">
<DataGridTemplateColumn.CellTemplate>
<HierarchicalDataTemplate>
<ComboBox ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type Window}},
Path=DataContext.Cities}"
DisplayMemberPath="Name"
SelectedValue="{Binding Path=RowEntries }"/>
</HierarchicalDataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Countries">
<DataGridTemplateColumn.CellTemplate>
<HierarchicalDataTemplate>
<ComboBox ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type Window}},
Path=DataContext.Countries}"
DisplayMemberPath="Name"/>
</HierarchicalDataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Button Content="Add Row" Margin="0,0,1797,0"></Button>
</StackPanel>

You should bind the SelectedValue property of the ComboBoxes to the CityName and CountryName properties of the RowEntry object and set the SelectedValuePath property of the ComboBoxes to "Name". Also set the UpdateSourcePropertyTrigger of the bindings to PropertyChanged:
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding AlternationIndex, RelativeSource={RelativeSource AncestorType=DataGridRow}}" Header="#"/>
<DataGridTemplateColumn Header="City">
<DataGridTemplateColumn.CellTemplate>
<HierarchicalDataTemplate>
<ComboBox ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.Cities}"
DisplayMemberPath="Name"
SelectedValuePath="Name"
SelectedValue="{Binding Path=CityName, UpdateSourceTrigger=PropertyChanged}"/>
</HierarchicalDataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Countries">
<DataGridTemplateColumn.CellTemplate>
<HierarchicalDataTemplate>
<ComboBox ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.Countries}"
DisplayMemberPath="Name"
SelectedValuePath="Name"
SelectedValue="{Binding Path=CountryName, UpdateSourceTrigger=PropertyChanged}"/>
</HierarchicalDataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
Then the setters of the source properties (CityName and CountryName) will be called when you select an item in the ComboBoxes.
If you want to select some values initially, you simply set these properties to some values that are present in the ComboBoxes:
public TestViewModel()
{
...
RowEntries.Add(new RowEntry() { CityName = "Sydney", CountryName = "Australia" });
}

Related

WPF MVVM datagrid update property on change of another

I'm trying to create a datagrid for creating an work assignment list (AssignmentPlanItem class), which has comboboxes for Employee, Assignment and Workcenter (all separate classes and foreign keys of the AssignmentPlanItem. The plan is filled straight to the datagrid. I know this might be easier if adding items was done through a form, but I think this is a snappy method, and I don't want to change it.
After numerous days on this issue I have got everything else working, but I also have a DefaultAssignmentId as a property of the Employee class, and I would like the DefaultAssignment to be fetched automatically to the datagrid's assignment field when the employee is selected. This is my first WPF application, so it might be that my code works only by some miraculous chance, so feel free to give general hints. I feel I have tried every possible combination for the bindings, so now I have to ask for help, as I couldn't find anything with Google.
XAML:
<Grid Margin="20">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="150"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<DataGrid x:Name="assignmentPlanItemsDataGrid" Margin="0,3,0,0" ItemsSource="{Binding DataGridRows, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, NotifyOnSourceUpdated=True}" AutoGenerateColumns="False" CanUserAddRows="False" SelectedItem="{Binding CurrentItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, NotifyOnSourceUpdated=True}" IsSynchronizedWithCurrentItem="False">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Employee" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Path = DataContext.EmployeeComboRows, RelativeSource={RelativeSource Findancestor, AncestorType={x:Type Window}}}"
SelectedItem="{Binding DataContext.SelectedEmployee,Mode=TwoWay, RelativeSource={RelativeSource Findancestor, AncestorType={x:Type Window}}}"
SelectedValue="{Binding EmployeeId, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
SelectedValuePath="Id"
IsEditable="True"
DisplayMemberPath="FullName">
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Assignment" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding DataContext.AssignmentComboRows, RelativeSource={RelativeSource Findancestor, AncestorType={x:Type Window}}}"
SelectedItem="{Binding DataContext.SelectedAssignment,Mode=TwoWay, RelativeSource={RelativeSource Findancestor, AncestorType={x:Type Window}}}"
SelectedValuePath="Id"
DisplayMemberPath="Description"
SelectedValue="{Binding AssignmentId, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
IsEditable="True"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Grid>
The ViewModel:
public class AssignmentPlanItemViewModel : ViewModelBase
{
DataContext context = new DataContext();
//the datagrid collection
private ObservableCollection<AssignmentPlanItem> _dataGridRows = new ObservableCollection<AssignmentPlanItem>();
//datagrid selected item
private AssignmentPlanItem _currentItem;
//combobox itemssource collections
public ObservableCollection<Employee> EmployeeComboRows { get; set; }
public ObservableCollection<Assignment> AssignmentComboRows { get; set; }
public ObservableCollection<WorkStation> WorkStationComboRows { get; set; }
//the source event for the current assignment plan
public Event CurrentEvent;
public AssignmentPlanItemViewModel()
{
//populate combobox collections
EmployeeComboRows = new ObservableCollection<Employee>(context.Employees);
AssignmentComboRows = new ObservableCollection<Assignment>(context.Assignments);
WorkStationComboRows = new ObservableCollection<WorkStation>(context.WorkStations);
//getting the current event (yes, non-MVVM, I know)
CurrentEvent = context.Events.Find(AssignmentPlanWindow.eventId);
var planItems = CurrentEvent.AssignmentPlans.Last().AssignmentPlanItems;
DataGridRows = new ObservableCollection<AssignmentPlanItem>(planItems);
}
public AssignmentPlanItem CurrentItem
{
get { return _currentItem; }
set
{
if (value != _currentItem)
{
_currentItem = value;
OnPropertyChanged("CurrentItem");
OnPropertyChanged("DataGridRows");
}
}
}
public ObservableCollection<AssignmentPlanItem> DataGridRows
{
get { return _dataGridRows; }
set
{
_dataGridRows = value;
OnPropertyChanged("DataGridRows");
}
}
private Employee _selectedEmployee;
public Employee SelectedEmployee
{
get
{
return _selectedEmployee;
}
set
{
if (CurrentItem != null)
{
_selectedEmployee = value;
if (_selectedEmployee != null)
{
CurrentItem.EmployeeId = _selectedEmployee.Id;
var defaultAssigment = context.Assignments.Find((int)_selectedEmployee.DefaultAssignmentId);
CurrentItem.Assignment = defaultAssigment;
CurrentItem.AssignmentId = (int)_selectedEmployee.DefaultAssignmentId;
OnPropertyChanged("CurrentItem");
}
}
}
}
private Assignment _selectedAssignment;
public Assignment SelectedAssignment
{
get
{
return _selectedAssignment;
}
set
{
if (CurrentItem != null)
{
_selectedAssignment = value;
if (_selectedAssignment != null)
{
CurrentItem.AssignmentId = _selectedAssignment.Id;
CurrentItem.Assignment = _selectedAssignment;
OnPropertyChanged("CurrentItem");
}
}
}
}
}
So, I use the SelectedEmployee and SelectedAssignment properties to try to change the selected item of the datagrid (CurrentItem). The item is changed, but the change is not updated to the grid. When I save the grid, close and get back, the assignment has also changed.
In the XAML Assignment Combobox I tried
<SelectedValue="{Binding DataContext.CurrentItem.AssignmentId, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource Findancestor, AncestorType={x:Type Window}}}"/>
which actually got the view updating, but it changed all the assignment fields for all the rows in the datagrid to the same value as in the CurrentItem, even though I had IsSynchronizedWithCurrentItem=False everywhere.
My model classes do not implement INotifyPropertyChanged and the ViewModelBase I ripped somewhere from the web.
So, can anyone tell me what I'm doing wrong?
OK, I got it working with the help of ΩmegaMan. The solution was to have AssignmentPlanItem inherit from ViewModelBase (i.e. implement INotifyPropertyChanged), and change the AssignmentId property from
public AssignmentId {get; set; }
to
private int _assignmentId;
public int AssignmentId
{
get { return _assignmentId; }
set
{
_assignmentId = value;
OnPropertyChanged("AssignmentId");
}
}
The datagrid comboboxes had to have the following setup (not quite sure still if there is something superfluous):
<DataGrid x:Name="assignmentPlanItemsDataGrid" Margin="0,3,0,0" ItemsSource="{Binding DataGridRows, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" AutoGenerateColumns="False" CanUserAddRows="False" SelectedItem="{Binding CurrentItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" IsSynchronizedWithCurrentItem="False">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Employee" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Path = DataContext.EmployeeComboRows, RelativeSource={RelativeSource Findancestor, AncestorType={x:Type Window}}}"
SelectedItem="{Binding DataContext.SelectedEmployee,Mode=OneWayToSource, RelativeSource={RelativeSource Findancestor, AncestorType={x:Type Window}}}"
SelectedValue="{Binding EmployeeId}"
SelectedValuePath="Id"
IsEditable="True"
DisplayMemberPath="FullName"
IsSynchronizedWithCurrentItem="False">
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Assignment" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding DataContext.AssignmentComboRows, RelativeSource={RelativeSource Findancestor, AncestorType={x:Type Window}}}"
SelectedItem="{Binding DataContext.SelectedAssignment,Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource Findancestor, AncestorType={x:Type Window}}}"
SelectedValuePath="Id"
DisplayMemberPath="Description"
SelectedValue="{Binding AssignmentId}"
IsEditable="True"
IsSynchronizedWithCurrentItem="False"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
And the SelectedEmployee in the ViewModel had the following code to change the Assignment:
private Employee _selectedEmployee;
public Employee SelectedEmployee
{
get
{
return _selectedEmployee;
}
set
{
if (CurrentItem!= null)
{
_selectedEmployee = value;
OnPropertyChanged("SelectedEmployee");
if (SelectedEmployee != null)
{
CurrentItem.EmployeeId = SelectedEmployee.Id;
var defaultAssigment = Context.Assignments.Find((int)SelectedEmployee.DefaultAssignmentId);
CurrentItem.AssignmentId = (int)SelectedEmployee.DefaultAssignmentId;
CurrentItem.Assignment = defaultAssigment;
}
}
}
There was still one tricky part, namely setting the ComboBox SelectedItem binding mode to OneWayToSource. Without this, all the comboboxes in the column would get the Assignment of the CurrentItem. So to my feeble understanding this means that the ComboBox binding mode handles the updating to the ViewModel and Model, and the property change notification on the Model takes it back to the view through the SelectedValue. I'm still not sure whether it works or should work like this, but anyway it functions fully the way I want.
There are many levels of INotifyPropertyChange which needs to be understood.
When assigning to a list type structure, the notify is only for when the reference to the list changes; aka a new list has been created. The notify event does not flag any changes to what is, or is not in the list, nor any individual items in the list which may have a property change.
An observable collection does send notifications on when an item in its list is added or removed, but not when an individual item in the list's property changes.
If you want an item's property to be reflected in the data grid after a change to its property, then that object instance must adhere to INotifyPropertyChanged and the property must call the PropertyChanged with its property name to be broadcasted.
What you most likely have is a DTO object which does not adhere to INotifyPropertyChanged hence even though the current instance is properly referenced in your Selected... reference, the control that contains/displays the specific property value has no way of knowing the property has been changed; because it only monitors for a change event of that properties name.
What you need to do in your situation is create a Partial class off of your working classes and add INotifyPropertyChanged to the partial and supply an override to properties with change calls (PropertyChanged("FirstName)` (or whatever your method call is)) which will need to show their change.
Though this doesn't speak to your direct situation, here is my blog article that does show how one can effectively use INotifyPropertyChanged. Its on a VM, but the same methods can be applied to your partial DTO object.
Xaml: ViewModel Main Page Instantiation and Loading Strategy for Easier Binding

Getting empty combobox dropdown in WPF

I'm trying to populate the dropdown list within the Grid column but it's empty. The Grid column is defined like this:
<DataGridTemplateColumn Header="Voucher Type" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding VoucherType}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox SelectedItem="{Binding VoucherType}" ItemsSource="{Binding Path=DataContext.VTypes, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
The context class has the source defined like this:
public static ObservableCollection<string> VType { get; } = new ObservableCollection<string>()
{
"Journal Voucher",
"Cash Received Voucher",
"Cash Payment Voucher",
"Bank Received Voucher",
"Bank Payment Voucher",
};
Can someone please point to what I'm doing wrong?
Thanks.
Here is simple code how to add combobox in DataGrid.
Xaml Code:
<Grid>
<DataGrid Margin="5" ItemsSource="{Binding}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
<DataGridTextColumn Header="Capital" Binding="{Binding Capital}"/>
<DataGridTextColumn Header="Time Zone" Binding="{Binding TimeZone}"/>
<DataGridTemplateColumn Header="Cities" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox Margin="2" ItemsSource="{Binding Cities}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
private ObservableCollection<State> states = new ObservableCollection<State>();
public MainWindow()
{
InitializeComponent();
states.Add(new State()
{
Name = "Maryland",
Capital = "Annapolis",
TimeZone = "Eastern",
Cities = new ObservableCollection<string>() { "Frederick", "Baltimore", "Rockville"}
});
DataContext = states;
}
}
Model:
public class State
{
public string Name
{ get; set; }
public string TimeZone
{ get; set; }
public string Capital
{ get; set; }
public ObservableCollection<string> Cities
{ get; set; }
}
Hope this code may be help you.

SelectAll In Datagrid

I found a nice solution for SelectAll Checkboxes in a datagrid using XAML only:
<DataGrid x:Name="TestGrid" Tag="false">
<DataGrid.Resources>
<DataTemplate x:Key="HeaderCheckbox">
<CheckBox Name="SelectAll" IsChecked="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGrid}, Path=Tag, Mode=TwoWay}" />
</DataTemplate>
<DataTemplate x:Key="ItemCheckbox">
<CheckBox IsChecked="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGrid}, Path=Tag, Mode=OneWay}" />
</DataTemplate>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn HeaderTemplate="{StaticResource HeaderCheckbox}" CellTemplate="{StaticResource ItemCheckbox}" />
<DataGridTextColumn Binding="{Binding FirstName}" />
</DataGrid.Columns>
</DataGrid>
Source: Complete XAML Solution For SelectAll In Datagrid
But it's my question... In above example, the ItemCheckbox is bound to tag property of Datagrid, then how do I to bind the ItemCheckbox to my data field inclusive?
Easiest solution I can see it to use properties in your view model instead of Tag. First in class that holds items create SelectAll property that will update accordingly all items when changed:
public class MyItemCollection : INotifyPropertyChanged
{
private readonly ObservableCollection<MyItem> _items;
public ICollection<MyItem> Items { get { return _items; } }
private bool _selectAll;
public bool SelectAll
{
get { return _selectAll; }
set
{
if (_selectAll != value)
{
_selectAll = value;
OnPropertyChanged("SelectAll");
foreach (var item in _items) item.IsSelected = value;
}
}
}
}
Then add IsSelected property to your item. It will be updated either by SelectAll property or CheckBox in DataGrid
public class MyItem : INotifyPropertyChanged
{
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected != value)
{
_isSelected = value;
OnPropertyChanged("IsSelected");
}
}
}
}
and then update your binding to point to new properties:
<DataGrid x:Name="TestGrid" ItemsSource="{Binding Items}">
<DataGrid.Resources>
<DataTemplate x:Key="HeaderCheckbox">
<CheckBox Name="SelectAll" IsChecked="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=DataContext.SelectAll}" />
</DataTemplate>
<DataTemplate x:Key="ItemCheckbox">
<CheckBox IsChecked="{Binding Path=IsSelected}" />
</DataTemplate>
</DataGrid.Resources>
</DataGrid>

WPF binding to combobox in DataGridTemplateColumn

I have a class in a WPF MVVM application which I would like to display in a popup form and manage some of its members.
public class Route
{
public string Name { get; set; }
public List<RouteSegment> MandatorySegments { get; set; }
}
public class RouteSegment
{
public decimal Id { get; set; }
public string Name { get; set; }
}
I've only added a few of the member fields, the rest are not relevant.
What I would like to do is to display all the items found in MandatorySegments list in a combobx in a datagrid so I can add and remove new members easily.
This is the code I have so far:
In my ViewModel
public RouteTest SelectedRoute { get; set; }
public ObservableCollection<RouteSegment> AllRouteSegments { get; private set; }
public RouteSegment SelectedMandatorySegment { get; set; }
In my View
<DataGrid Grid.RowSpan="2"
AutoGenerateColumns="False" HorizontalAlignment="Left" Margin="5,6,0,5" Name="dgMandatorySegments" VerticalAlignment="Stretch" Width="306"
ItemsSource="{Binding SelectedRouteTest.MandatorySegments, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" CanUserResizeRows="False" CanUserSortColumns="False" CanUserResizeColumns="False" CanUserReorderColumns="False"
CanUserAddRows="{StaticResource False}" SelectionMode="Single" SelectedItem="{Binding SelectedMandatorySegment, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Route Segment">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding DataContext.AllRouteSegments, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
ItemTemplate="{StaticResource CboxItemTemplate}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
What I'm missing is the SelectedValue or SelectedItem on the combobox in the templated grid column. Now, in the grid I have the same number of rows as elements in my list and the ItemsSource of the combobox binds properly, because I can choose from all the RouteSegment elements. But the initial value is always empty for all the rows in the datagrid.
What should the SelectedValue of the combobox bind to?
This depends on whether you want the same default value for all rows - or row-independent initial values. I would recommend you use the SelectedItem to avoid issues with SelectedValue not working reliably. The current VM you have only allows for one selected mandatory segment - given this assumption, you are only setup for one default value for all grid rows.
<ComboBox ItemsSource="{Binding DataContext.AllRouteSegments, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
SelectedItem="{Binding DataContext.SelectedMandatorySegment, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
ItemTemplate="{StaticResource CboxItemTemplate}"/>
DisplayMemberPath is probably what you want. The following ComboBox works nicely in a DataGrid in my WPF application. (Please note that you need to update the binding path to make it work in your application.)
<ComboBox ItemsSource="{Binding Path=AllRouteSegments}" DisplayMemberPath="Name" SelectedItem="{Binding Path=SelectedMandatorySegment, UpdateSourceTrigger=PropertyChanged}"/>

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