GridView is not adding rows to collection - wpf

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.

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

WPF MVVM Databinding Nested Datagrid

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" });
}

Checkbox with DataGrid WPF

I am trying to create a DataGrid in WPF 4.0 using MVVM...
Features required -
Muti - Select rows using a checkbox (single click)
A select all checkbox to check all the checkboxes in the datagrid
Something like this -
It has been 2 days and i am not able to figure out how to solve the problem effectively..
A working example is what I need now ASAP..
I'll highly appreciate if someone has a working solution to share with me...
N please don't tell me to google this thing because none of the things worked out for me...
UPDATE -
I am using AutoGeneration of Columns
I don't want to add "IsSelected" or any of such property in my MODEL..
I am just facing 2 problems -
Firstly, "Select all" Feature i.e. checking all checkboxes on the checkbox click of the one present in the column header...(I am able to select and unselect the datagrid but not able to tick/untick the checkboxes)
Secondly, Multiple selection on mouse click without holding Ctrl Key..
When you're working with MVVM, you have to be aware of what is considered data and what is strictly UI.
Is your SelectedItems going to be part of your data, or only your UI?
If it's part of your data, you really should have an IsSelected property on your data model, even if that means extending the data class to include an IsSelected property, or creating a wrapper class that only contains bool IsSelected and object MyDataItem. The first option is probably preferred, since you could keep AutoGenerateColumns="True", and it makes the column bindings simpler.
Then you would just bind your DataGridRow.SelectedItem to the IsSelected property of the data item:
<Style TargetType="{x:Type DataGridRow}">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
But if your SelectedItems is only for the UI, or if you are breaking the MVVM pattern for some reason in this instance, than you can create the unbound CheckBox and use some code behind to ensure the CheckBox is correctly synchronized to the SelectedItem.
I did a quick sample app, and here is what my code looked like:
First off, I just added the unbound CheckBox column to the column list using a DataGridTemplateColumn. This will get added before the AutoGenerateColumns list of columns.
<DataGrid x:Name="TestDataGrid" ItemsSource="{Binding Test}"
SelectionMode="Extended" CanUserAddRows="False"
PreviewMouseLeftButtonDown="TestDataGrid_PreviewMouseLeftButtonDown_1">
<DataGrid.Columns>
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox x:Name="TestCheckBox"
PreviewMouseLeftButtonDown="CheckBox_PreviewMouseLeftButtonDown" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
Second, I added a PreviewMouseDown event to the CheckBox to make it set the IsSelected property of the row.
private void CheckBox_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var chk = (CheckBox)sender;
var row = VisualTreeHelpers.FindAncestor<DataGridRow>(chk);
var newValue = !chk.IsChecked.GetValueOrDefault();
row.IsSelected = newValue;
chk.IsChecked = newValue;
// Mark event as handled so that the default
// DataGridPreviewMouseDown doesn't handle the event
e.Handled = true;
}
It needs to navigate the VisualTree to find the DataGridRow associated with the clicked CheckBox to select it, and to make life easier I am using some custom VisualTreeHelpers that I have on my blog to find the DataGridRow. You can use the same code, or you can create your own method for searching the VisualTree.
And last of all, if the user clicks on anywhere other than the CheckBox, we want to disable the default DataGrid selection event. This ensures that the IsSelected value will only change when you click on the CheckBox.
There are multiple ways of doing this that will disable the selection at different levels, but to make life simple I just disabled the DataGrid.PreviewMouseLeftButtonDown event if the user didn't click on the CheckBox.
private void TestDataGrid_PreviewMouseLeftButtonDown_1(object sender, MouseButtonEventArgs e)
{
var chk = VisualTreeHelpers.FindAncestor<CheckBox>((DependencyObject)e.OriginalSource, "TestCheckBox");
if (chk == null)
e.Handled = true;
}
I using my custom VisualTreeHelpers again to navigate the visual tree and find out if the CheckBox was clicked on, and cancelling the event if the user clicked on anywhere other than the CheckBox.
As for your 2nd request of adding a CheckBox to SelectAll or UnselectAll items, this would once again be dependent on if your selection is part of the UI or the data.
If it's part of the UI, simply add a CheckBox to the DataGridTemplateColumn.HeaderTemplate, and when it's clicked, loop through the DataGrid.Rows, find the CheckBox in the first column, and check or uncheck it.
If it's part of the data you could still do the same thing (only set the bound value in the DataGrid.Items instead of the CheckBox.IsChecked from the DataGrid.Rows), or you could do as Adolfo Perez suggested, and bind it to a property on the ViewModel.
For an MVVM Solution you could try this:
<StackPanel>
<DataGrid ItemsSource="{Binding Path=TestItems}" AutoGenerateColumns="False" Name="MyDataGrid"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridCheckBoxColumn Binding="{Binding IsSelected}" Width="50" >
<DataGridCheckBoxColumn.HeaderTemplate>
<DataTemplate x:Name="dtAllChkBx">
<CheckBox Name="cbxAll" Content="All" IsChecked="{Binding Path=DataContext.AllSelected,RelativeSource={RelativeSource AncestorType=DataGrid}}"/>
</DataTemplate>
</DataGridCheckBoxColumn.HeaderTemplate>
</DataGridCheckBoxColumn>
<DataGridTemplateColumn Header="Name" Width="SizeToCells" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
In your ViewModel:
private void PopulateTestItems()
{
TestItems = new ObservableCollection<TestItem>();
for (int i = 0; i < 5; i++)
{
TestItem ti = new TestItem();
ti.Name = "TestItem" + i;
ti.IsSelected = true;
TestItems.Add(ti);
}
}
private bool _AllSelected;
public bool AllSelected
{
get { return _AllSelected; }
set
{
_AllSelected = value;
TestItems.ToList().ForEach(x => x.IsSelected = value);
NotifyPropertyChanged(m => m.AllSelected);
}
}
private ObservableCollection<TestItem> _TestItems;
public ObservableCollection<TestItem> TestItems
{
get { return _TestItems; }
set
{
_TestItems = value;
NotifyPropertyChanged(m => m.TestItems);
}
}
And finally the sample Model class:
public class TestItem : ModelBase<TestItem>
{
private string _Name;
public string Name
{
get { return _Name; }
set
{
_Name = value;
NotifyPropertyChanged(m => m.Name);
}
}
private bool _IsSelected;
public bool IsSelected
{
get { return _IsSelected; }
set
{
_IsSelected = value;
NotifyPropertyChanged(m => m.IsSelected);
}
}
}
Most of the code above should be self-explanatory but if you have any question let me know
Your view can be something like
<DataGrid Name="SomeDataGrid" Grid.Row="0" ItemsSource="{Binding Path=SomeCollection}">
<DataGrid.Columns>
<DataGridTemplateColumn>
<DataGridTemplateColumn.HeaderTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding
RelativeSource={RelativeSource AncestorType={x:Type DataGrid}},
Path=DataContext.AllItemsAreChecked}" />
</DataTemplate>
</DataGridTemplateColumn.HeaderTemplate>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="{x:Type local:SomeType}">
<CheckBox Focusable="False" IsChecked="{Binding Path=IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Left" VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="RandomNumber" Width="160">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="{x:Type local:SomeType}">
<TextBlock Text="{Binding Path=RandomNumber}" TextWrapping="Wrap" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Date" Width="160">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="{x:Type local:SomeType}">
<TextBlock Text="{Binding Path=Date}" TextWrapping="Wrap" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Time" Width="50">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="{x:Type local:SomeType}">
<TextBlock Text="{Binding Time}" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
And in viewmodel
SomeCollection binding property is an observablecollection
sometype contains properties like IsSelected , RandomNumber ,Date , Time
for eg:
class ViewModel
{
public ObservableCollection<SomeType> SomeCollection{get;set;}
}
class SomeType
{
public string Date {get;set;}
public string Time {get;set;}
public string RandomNumber {get;set;}
public bool IsSelected {get;set;}
}

Adding new DataGrid row at runtime with Cascading Combo boxes?

Bear with me if you would, I'm brand new to WPF MVVM development.
The utility I'm working on (WPF client application) contains a 4 column datagrid that displays the currently available data to the user for a specified period of time. The records returned may contain all or just some of the data ultimately required for the record to be complete. The user should be able to modify the missing pieces or add new rows altogether to fill in missing records. 3 of the 4 columns contain combo boxes with only valid choices. The combo boxes are cascading so that the first combo determines the contents of combo 2 and 3. The 4th column is a simple price column.
I've managed to get the grid working perfectly with the default return data. The combox populate correctly as well as display the current record's value as the selected item in the combo.
My problem is with adding a new row. I've been trying to accomplish this inline. Meaning the user clicks on or tabs to the blank row at the bottom of the list and a new row gets added to collection accepts input. The first combo is populated in the empty row, but selecting a value does not cascade the changes to the other 2 combos. It works up in the populated rows though.
That make sense? I'm probably missing something so stupid simple that I'll kick myself when someone points it out but right now I'm tearing out what little hair I have left.
So, Is it possible to have a new user add a row just by tabbing through to the next line?
Do I need to anything special to wire up the cascading combo boxes in the new row?
Thanks!
Here's my XAML (essentially pseudo code after stripping it down so don't mind mispellings):
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApplication2"
mc:Ignorable="d"
d:DesignHeight="768"
d:DesignWidth="1024">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<StackPanel>
<DataGrid AutoGenerateColumns="False"
ItemsSource="{Binding Prices}"
SelectedItem="{Binding SelectedPrice}"
CanUserAddRows="True">
<DataGrid.Columns>
<!-- Price Group -->
<DataGridTemplateColumn MinWidth="120"
Header="Price Group">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox x:Name="cboPriceGroup"
ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}, Path=DataContext.PriceGroups}"
DisplayMemberPath="Name"
SelectedItem="{Binding PriceGroupObject, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- Option 1 -->
<DataGridTemplateColumn MinWidth="120"
Header="Option1">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox x:Name="cboOption1"
ItemsSource="{Binding PriceGroupObject.Options1}"
DisplayMemberPath="Code"
SelectedValuePath="Id"
SelectedValue="{Binding Option1Id, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- Option 2 -->
<DataGridTemplateColumn MinWidth="120"
Header="Option2">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox x:Name="cboOption2"
ItemsSource="{Binding PriceGroupObject.Options2}"
DisplayMemberPath="Code"
SelectedValuePath="Id"
SelectedValue="{Binding Option2Id, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- Price -->
<DataGridTemplateColumn MinWidth="120"
Header="Price">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox x:Name="txtPrice"
Text="{Binding Price, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Grid>
And here's the ViewModel:
// ViewModelBase implements INotifyPropertyChanged
public class MainWindowViewModel : ViewModelBase
{
#region Declarations
// Comes From Backend.Services
private IPriceListService _PriceListService;
private ObservableCollection<Price> _Prices;
private Price _SelectedPrice;
private IEnumerable<PriceGroup> _PriceGroups;
private PriceGroup _SelectedPriceGroup;
// Same for Options1, Options2
#endregion
#region Properties
public ObservableCollection<Price> Prices
{
get { return _Prices; }
set
{
if (_Prices != value)
{
_Prices = value;
OnPropertyChanged("Prices");
}
}
}
public Price SelectedPrice
{
get { return _Price; }
set
{
if (_Price != value)
{
_Price = value;
OnPropertyChanged("SelectedPrice");
}
}
}
// Same for PriceGroups, Options1, Options2
#endregion
#region Constructor
public MainViewModel()
: this(new FakePriceListService())
{
}
// Constructor
public MainViewModel(IPriceListService priceListService)
{
_PriceListService = priceListService;
InitializeViewModel();
}
protected override void InitializeViewModel()
{
// Test ID
LoadFuturesPrices(12345);
LoadPriceGroups();
base.InitializeViewModel();
}
#endregion
#region Methods
void LoadFuturesPrices(short FinancialPeriodId)
{
Prices = _PriceListService.GetFuturesPriceList(FinancialPeriodId);
}
void LoadPriceGroups()
{
PriceGroups = _PriceListService.GetPriceGroups();
}
void PriceGroupSelected(string PriceGroupId)
{
SelectedPriceGroup = (from p in PriceGroups where p.Name == PriceGroupId select p).SingleOrDefault();
}
#endregion
}

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